1# ----------------------------------------------------------------------------
2# pyglet
3# Copyright (c) 2006-2008 Alex Holkner
4# Copyright (c) 2008-2020 pyglet contributors
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10#
11#  * Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13#  * Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in
15#    the documentation and/or other materials provided with the
16#    distribution.
17#  * Neither the name of pyglet nor the names of its
18#    contributors may be used to endorse or promote products
19#    derived from this software without specific prior written
20#    permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33# POSSIBILITY OF SUCH DAMAGE.
34# ----------------------------------------------------------------------------
35import platform
36from ctypes import c_uint32, c_int, byref
37
38from pyglet.gl.base import Config, CanvasConfig, Context
39
40from pyglet.gl import ContextException
41from pyglet.gl import gl
42from pyglet.gl import agl
43
44from pyglet.canvas.cocoa import CocoaCanvas
45
46from pyglet.libs.darwin import cocoapy, quartz
47
48
49NSOpenGLPixelFormat = cocoapy.ObjCClass('NSOpenGLPixelFormat')
50NSOpenGLContext = cocoapy.ObjCClass('NSOpenGLContext')
51
52# Version info, needed as OpenGL different Lion and onward
53"""Version is based on Darwin kernel, not OS-X version.
54OS-X / Darwin version history
55http://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history
56pre-release:    0.1, 0.2, 1.0, 1.1,
57kodiak:         1.2.1,
58cheetah:        1.3.1,
59puma:           1.4.1, 5.1 -> 5.5
60jaguar:         6.0.1 -> 6.8
61panther:        7.0 -> 7.9
62tiger:          8.0 -> 8.11
63leopard:        9.0 -> 9.8
64snow_leopard:   10.0 -> 10.8
65lion:           11.0 -> 11.4
66mountain_lion:  12.0 -> 12.5
67mavericks:      13.0 -> 13.4
68yosemite:       14.0 -> 14.5
69el_capitan:     15.0 -> 15.6
70sierra:         16.0 -> 16.6
71"""
72os_x_release = {
73    'pre-release':      (0,1),
74    'kodiak':           (1,2,1),
75    'cheetah':          (1,3,1),
76    'puma':             (1,4.1),
77    'jaguar':           (6,0,1),
78    'panther':          (7,),
79    'tiger':            (8,),
80    'leopard':          (9,),
81    'snow_leopard':     (10,),
82    'lion':             (11,),
83    'mountain_lion':    (12,),
84    'mavericks':        (13,),
85    'yosemite':         (14,),
86    'el_capitan':       (15,),
87    'sierra':           (16,),
88    }
89
90def os_x_version():
91    version = tuple([int(v) for v in platform.release().split('.')])
92
93    # ensure we return a tuple
94    if len(version) > 0:
95        return version
96    return (version,)
97
98_os_x_version = os_x_version()
99
100# Valid names for GL attributes and their corresponding NSOpenGL constant.
101_gl_attributes = {
102    'double_buffer': cocoapy.NSOpenGLPFADoubleBuffer,
103    'stereo': cocoapy.NSOpenGLPFAStereo,
104    'buffer_size': cocoapy.NSOpenGLPFAColorSize,
105    'sample_buffers': cocoapy.NSOpenGLPFASampleBuffers,
106    'samples': cocoapy.NSOpenGLPFASamples,
107    'aux_buffers': cocoapy.NSOpenGLPFAAuxBuffers,
108    'alpha_size': cocoapy.NSOpenGLPFAAlphaSize,
109    'depth_size': cocoapy.NSOpenGLPFADepthSize,
110    'stencil_size': cocoapy.NSOpenGLPFAStencilSize,
111
112    # Not exposed by pyglet API (set internally)
113    'all_renderers': cocoapy.NSOpenGLPFAAllRenderers,
114    'fullscreen': cocoapy.NSOpenGLPFAFullScreen,
115    'minimum_policy': cocoapy.NSOpenGLPFAMinimumPolicy,
116    'maximum_policy': cocoapy.NSOpenGLPFAMaximumPolicy,
117    'screen_mask' : cocoapy.NSOpenGLPFAScreenMask,
118
119    # Not supported in current pyglet API
120    'color_float': cocoapy.NSOpenGLPFAColorFloat,
121    'offscreen': cocoapy.NSOpenGLPFAOffScreen,
122    'sample_alpha': cocoapy.NSOpenGLPFASampleAlpha,
123    'multisample': cocoapy.NSOpenGLPFAMultisample,
124    'supersample': cocoapy.NSOpenGLPFASupersample,
125}
126
127# NSOpenGL constants which do not require a value.
128_boolean_gl_attributes = frozenset([
129    cocoapy.NSOpenGLPFAAllRenderers,
130    cocoapy.NSOpenGLPFADoubleBuffer,
131    cocoapy.NSOpenGLPFAStereo,
132    cocoapy.NSOpenGLPFAMinimumPolicy,
133    cocoapy.NSOpenGLPFAMaximumPolicy,
134    cocoapy.NSOpenGLPFAOffScreen,
135    cocoapy.NSOpenGLPFAFullScreen,
136    cocoapy.NSOpenGLPFAColorFloat,
137    cocoapy.NSOpenGLPFAMultisample,
138    cocoapy.NSOpenGLPFASupersample,
139    cocoapy.NSOpenGLPFASampleAlpha,
140])
141
142# Attributes for which no NSOpenGLPixelFormatAttribute name exists.
143# We could probably compute actual values for these using
144# NSOpenGLPFAColorSize / 4 and NSOpenGLFAAccumSize / 4, but I'm not that
145# confident I know what I'm doing.
146_fake_gl_attributes = {
147    'red_size': 0,
148    'green_size': 0,
149    'blue_size': 0,
150    'accum_red_size': 0,
151    'accum_green_size': 0,
152    'accum_blue_size': 0,
153    'accum_alpha_size': 0
154}
155
156class CocoaConfig(Config):
157
158    def match(self, canvas):
159        # Construct array of attributes for NSOpenGLPixelFormat
160        attrs = []
161        for name, value in self.get_gl_attributes():
162            attr = _gl_attributes.get(name)
163            if not attr or not value:
164                continue
165            attrs.append(attr)
166            if attr not in _boolean_gl_attributes:
167                attrs.append(int(value))
168
169        # Support for RAGE-II, which is not compliant.
170        attrs.append(cocoapy.NSOpenGLPFAAllRenderers)
171
172        # Force selection policy.
173        attrs.append(cocoapy.NSOpenGLPFAMaximumPolicy)
174
175        # NSOpenGLPFAFullScreen is always supplied so we can switch to and
176        # from fullscreen without losing the context.  Also must supply the
177        # NSOpenGLPFAScreenMask attribute with appropriate display ID.
178        # Note that these attributes aren't necessary to render in fullscreen
179        # on Mac OS X 10.6, because there we are simply rendering into a
180        # screen sized window.  See:
181        # http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_fullscreen/opengl_cgl.html%23//apple_ref/doc/uid/TP40001987-CH210-SW6
182        # Otherwise, make sure we refer to the correct Profile for OpenGL (Core or
183        # Legacy) on Lion and afterwards
184        if _os_x_version < os_x_release['snow_leopard']:
185            attrs.append(cocoapy.NSOpenGLPFAFullScreen)
186            attrs.append(cocoapy.NSOpenGLPFAScreenMask)
187            attrs.append(quartz.CGDisplayIDToOpenGLDisplayMask(quartz.CGMainDisplayID()))
188        elif _os_x_version >= os_x_release['lion']:
189            # check for opengl profile
190            # This requires OS-X Lion (Darwin 11) or higher
191            version = (
192                getattr(self, 'major_version', None) or 2,
193                getattr(self, 'minor_version', None)
194                )
195            # tell os-x we want to request a profile
196            attrs.append(cocoapy.NSOpenGLPFAOpenGLProfile)
197
198            # check if we're wanting core or legacy
199            # Mavericks (Darwin 13) and up are capable of the Core 4.1 profile,
200            # while Lion and up are only capable of Core 3.2
201            if version[0] >= 4 and _os_x_version >= os_x_release['mavericks']:
202                attrs.append(int(cocoapy.NSOpenGLProfileVersion4_1Core))
203            elif version[0] >= 3:
204                attrs.append(int(cocoapy.NSOpenGLProfileVersion3_2Core))
205            else:
206                attrs.append(int(cocoapy.NSOpenGLProfileVersionLegacy))
207        # Terminate the list.
208        attrs.append(0)
209
210        # Create the pixel format.
211        attrsArrayType = c_uint32 * len(attrs)
212        attrsArray = attrsArrayType(*attrs)
213        pixel_format = NSOpenGLPixelFormat.alloc().initWithAttributes_(attrsArray)
214
215        # Return the match list.
216        if pixel_format is None:
217            return []
218        else:
219            return [CocoaCanvasConfig(canvas, self, pixel_format)]
220
221
222class CocoaCanvasConfig(CanvasConfig):
223
224    def __init__(self, canvas, config, pixel_format):
225        super(CocoaCanvasConfig, self).__init__(canvas, config)
226        self._pixel_format = pixel_format
227
228        # Query values for the attributes of the pixel format, and then set the
229        # corresponding attributes of the canvas config.
230        for name, attr in _gl_attributes.items():
231            vals = c_int()
232            self._pixel_format.getValues_forAttribute_forVirtualScreen_(byref(vals), attr, 0)
233            setattr(self, name, vals.value)
234
235        # Set these attributes so that we can run pyglet.info.
236        for name, value in _fake_gl_attributes.items():
237            setattr(self, name, value)
238
239        # Update the minor/major version from profile if (Mountain)Lion
240        if _os_x_version >= os_x_release['lion']:
241            vals = c_int()
242            profile = self._pixel_format.getValues_forAttribute_forVirtualScreen_(
243                byref(vals),
244                cocoapy.NSOpenGLPFAOpenGLProfile,
245                0
246                )
247
248            if vals.value == cocoapy.NSOpenGLProfileVersion4_1Core:
249                setattr(self, "major_version", 4)
250                setattr(self, "minor_version", 1)
251            elif vals.value == cocoapy.NSOpenGLProfileVersion3_2Core:
252                setattr(self, "major_version", 3)
253                setattr(self, "minor_version", 2)
254            else:
255                setattr(self, "major_version", 2)
256                setattr(self, "minor_version", 1)
257
258    def create_context(self, share):
259        # Determine the shared NSOpenGLContext.
260        if share:
261            share_context = share._nscontext
262        else:
263            share_context = None
264
265        # Create a new NSOpenGLContext.
266        nscontext = NSOpenGLContext.alloc().initWithFormat_shareContext_(
267            self._pixel_format,
268            share_context)
269
270        return CocoaContext(self, nscontext, share)
271
272    def compatible(self, canvas):
273        return isinstance(canvas, CocoaCanvas)
274
275
276class CocoaContext(Context):
277
278    def __init__(self, config, nscontext, share):
279        super(CocoaContext, self).__init__(config, share)
280        self.config = config
281        self._nscontext = nscontext
282
283    def attach(self, canvas):
284        # See if we want OpenGL 3 in a non-Lion OS
285        if _os_x_version < os_x_release['lion'] and self.config.requires_gl_3():
286            raise ContextException('OpenGL 3 not supported')
287
288        super(CocoaContext, self).attach(canvas)
289        # The NSView instance should be attached to a nondeferred window before calling
290        # setView, otherwise you get an "invalid drawable" message.
291        self._nscontext.setView_(canvas.nsview)
292        self._nscontext.view().setWantsBestResolutionOpenGLSurface_(1)
293        self.set_current()
294
295    def detach(self):
296        super(CocoaContext, self).detach()
297        self._nscontext.clearDrawable()
298
299    def set_current(self):
300        self._nscontext.makeCurrentContext()
301        super(CocoaContext, self).set_current()
302
303    def update_geometry(self):
304        # Need to call this method whenever the context drawable (an NSView)
305        # changes size or location.
306        self._nscontext.update()
307
308    def set_full_screen(self):
309        self._nscontext.makeCurrentContext()
310        self._nscontext.setFullScreen()
311
312    def destroy(self):
313        super(CocoaContext, self).destroy()
314        self._nscontext.release()
315        self._nscontext = None
316
317    def set_vsync(self, vsync=True):
318        vals = c_int(vsync)
319        self._nscontext.setValues_forParameter_(byref(vals), cocoapy.NSOpenGLCPSwapInterval)
320
321    def get_vsync(self):
322        vals = c_int()
323        self._nscontext.getValues_forParameter_(byref(vals), cocoapy.NSOpenGLCPSwapInterval)
324        return vals.value
325
326    def flip(self):
327        self._nscontext.flushBuffer()
328