diff --git a/src/python/magnum/gl.cpp b/src/python/magnum/gl.cpp index 5a1480c..262ac4a 100644 --- a/src/python/magnum/gl.cpp +++ b/src/python/magnum/gl.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,27 @@ namespace magnum { namespace { +/* Stores additional stuff needed for proper refcounting of applications that + own current context. The context itself isn't deleted, that's a + responsibility of the applications instead. For some reason it *has to be* + templated, otherwise PYBIND11_DECLARE_HOLDER_TYPE doesn't work. */ +template struct ContextHolder: std::unique_ptr { + static_assert(std::is_same::value, "context holder has to hold a context"); + + /* Used when instantiating a context directly */ + explicit ContextHolder(T* object): std::unique_ptr{object} {} + + /* Used by Context.current() */ + explicit ContextHolder(T* object, pybind11::object owner) noexcept: std::unique_ptr{object}, owner{std::move(owner)} {} + + ContextHolder(ContextHolder&&) noexcept = default; + ContextHolder(const ContextHolder&) = delete; + ContextHolder& operator=(ContextHolder&&) noexcept = default; + ContextHolder& operator=(const ContextHolder&) = default; + + pybind11::object owner; +}; + /* Otherwise pybind yells that `generic_type: type "Framebuffer" has a non-default holder type while its base "Magnum::GL::AbstractFramebuffer" does not` -- we're using PyFramebufferHolder for it */ @@ -61,6 +83,7 @@ template struct NonDefaultFramebufferHolder: std::unique_ptr) PYBIND11_DECLARE_HOLDER_TYPE(T, magnum::NonDefaultFramebufferHolder) namespace magnum { @@ -278,6 +301,119 @@ void gl(py::module_& m) { .def("version", static_cast(*)(GL::Version)>(GL::version), "Major and minor version number from enum value", py::arg("version")) .def("is_version_es", GL::isVersionES, "Whether given version is OpenGL ES or WebGL"); + /* Context */ + { + py::class_> context{m, "Context", "Magnum OpenGL context"}; + + py::enum_ contextFlag{context, "Flag", "Context flag"}; + contextFlag + .value("DEBUG", GL::Context::Flag::Debug) + #ifndef MAGNUM_TARGET_GLES + .value("FORWARD_COMPATIBLE", GL::Context::Flag::ForwardCompatible) + #endif + .value("NO_ERROR", GL::Context::Flag::NoError) + #ifndef MAGNUM_TARGET_GLES2 + .value("ROBUST_ACCESS", GL::Context::Flag::RobustAccess) + #endif + .value("NONE", GL::Context::Flag{}); + corrade::enumOperators(contextFlag); + + py::enum_ contextState{context, "State", "State to reset"}; + contextState + .value("BUFFERS", GL::Context::State::Buffers) + #ifndef MAGNUM_TARGET_GLES2 + .value("UNBIND_PIXEL_BUFFER", GL::Context::State::UnbindPixelBuffer) + #endif + .value("FRAMEBUFFERS", GL::Context::State::Framebuffers) + .value("MESHES", GL::Context::State::Meshes) + .value("MESH_VAO", GL::Context::State::MeshVao) + .value("BIND_SCRATCH_VAO", GL::Context::State::BindScratchVao) + .value("PIXEL_STORAGE", GL::Context::State::PixelStorage) + .value("RENDERER", GL::Context::State::Renderer) + .value("SHADERS", GL::Context::State::Shaders) + .value("TEXTURES", GL::Context::State::Textures) + #ifndef MAGNUM_TARGET_GLES2 + .value("TRANSFORM_FEEDBACK", GL::Context::State::TransformFeedback) + #endif + .value("ENTER_EXTERNAL", GL::Context::State::EnterExternal) + .value("EXIT_EXTERNAL", GL::Context::State::ExitExternal); + corrade::enumOperators(contextState); + + py::enum_ contextDetectedDriver{context, "DetectedDriver", "Detected driver"}; + contextDetectedDriver + #ifndef MAGNUM_TARGET_WEBGL + .value("AMD", GL::Context::DetectedDriver::Amd) + #endif + #ifdef MAGNUM_TARGET_GLES + .value("ANGLE", GL::Context::DetectedDriver::Angle) + #endif + #ifndef MAGNUM_TARGET_WEBGL + .value("INTEL_WINDOWS", GL::Context::DetectedDriver::IntelWindows) + .value("MESA", GL::Context::DetectedDriver::Mesa) + .value("NVIDIA", GL::Context::DetectedDriver::NVidia) + .value("SVGA3D", GL::Context::DetectedDriver::Svga3D) + #ifdef MAGNUM_TARGET_GLES + .value("SWIFTSHADER", GL::Context::DetectedDriver::SwiftShader) + #endif + #endif + #ifdef CORRADE_TARGET_ANDROID + .value("ARM_MALI", GL::Context::DetectedDriver::ArmMali) + #endif + ; + corrade::enumOperators(contextDetectedDriver); + + context + .def_property_readonly_static("has_current", [](py::object) { + return GL::Context::hasCurrent(); + }, "Whether there is any current context") + .def_property_readonly_static("current", [](py::object) { + if(!GL::Context::hasCurrent()) { + PyErr_SetString(PyExc_RuntimeError, "no current context"); + throw py::error_already_set{}; + } + + py::object owner = py::none{}; + auto* glContextOwner = reinterpret_cast*>(py::get_shared_data("magnumGLContextOwner")); + if(glContextOwner && glContextOwner->first) { + CORRADE_INTERNAL_ASSERT(glContextOwner->second); + owner = Corrade::pyObjectFromInstance(glContextOwner->first, *glContextOwner->second); + } + + return ContextHolder{&GL::Context::current(), owner}; + }, "Current context") + /** @todo context switching (needs additions to the "who owns + current context instance" variable -- a map?) */ + .def_property_readonly("version", &GL::Context::version, "OpenGL version") + .def_property_readonly("vendor_string", &GL::Context::vendorString, "Vendor string") + .def_property_readonly("renderer_string", &GL::Context::rendererString, "Renderer string") + .def_property_readonly("version_string", &GL::Context::versionString, "Version string") + .def_property_readonly("shading_language_version_string", &GL::Context::shadingLanguageVersionString, "Shading language version string") + .def_property_readonly("shading_language_version_strings", &GL::Context::shadingLanguageVersionStrings, "Shading language version strings") + .def_property_readonly("extension_strings", &GL::Context::extensionStrings, "Extension strings") + #ifndef MAGNUM_TARGET_WEBGL + .def_property_readonly("flags", &GL::Context::flags, "Context flags") + #endif + /** @todo supportedExtensions() (needs Extension exposed) */ + #ifndef MAGNUM_TARGET_GLES + .def_property_readonly("is_core_profile", &GL::Context::isCoreProfile, "Detect if current OpenGL context is a core profile") + #endif + .def("is_version_supported", &GL::Context::isVersionSupported, "Get supported OpenGL version", py::arg("version")) + /** @todo supportedVersion() (takes an initializer list, add an + arrayview overload */ + /** @todo isExtensionSupported(), isExtensionDisabled() (needs + Extension exposed) */ + .def("reset_state", [](GL::Context& self, GL::Context::State states) { + self.resetState(states); + }, "Reset internal state tracker", py::arg("states") = GL::Context::State{}) + .def_property_readonly("detected_driver", [](GL::Context& self) { + return GL::Context::DetectedDriver(UnsignedShort(self.detectedDriver())); + }, "Detected driver") + + .def_property_readonly("owner", [](GL::Context& self) { + return pyObjectHolderFor(self).owner; + }, "Magnum Application owning the context"); + } + /* Shader (used by AbstractShaderProgram, so needs to be before) */ { py::class_ shader{m, "Shader", "Shader"}; diff --git a/src/python/magnum/platform/application.h b/src/python/magnum/platform/application.h index fe25ef1..3671591 100644 --- a/src/python/magnum/platform/application.h +++ b/src/python/magnum/platform/application.h @@ -32,7 +32,7 @@ namespace magnum { namespace platform { -template void application(py::class_& c) { +template void application(py::class_& c) { py::class_ configuration{c, "Configuration", "Configuration"}; configuration .def(py::init()) diff --git a/src/python/magnum/platform/egl.cpp b/src/python/magnum/platform/egl.cpp index 6aab7a8..b8dac5a 100644 --- a/src/python/magnum/platform/egl.cpp +++ b/src/python/magnum/platform/egl.cpp @@ -28,6 +28,7 @@ #include "magnum/bootstrap.h" #include "magnum/platform/windowlessapplication.h" +#include "magnum/platform/holder.h" namespace magnum { namespace platform { @@ -64,7 +65,7 @@ void egl(py::module_& m) { virtual ~PyWindowlessApplication() {} }; - py::class_ windowlessEglApplication{m, "WindowlessApplication", "Windowless EGL application"}; + py::class_> windowlessEglApplication{m, "WindowlessApplication", "Windowless EGL application"}; windowlessapplication(windowlessEglApplication); } diff --git a/src/python/magnum/platform/glfw.cpp b/src/python/magnum/platform/glfw.cpp index f48243a..3f6044d 100644 --- a/src/python/magnum/platform/glfw.cpp +++ b/src/python/magnum/platform/glfw.cpp @@ -30,6 +30,7 @@ #include "magnum/bootstrap.h" #include "magnum/platform/application.h" +#include "magnum/platform/holder.h" namespace magnum { namespace platform { @@ -155,7 +156,7 @@ void glfw(py::module_& m) { } }; - py::class_ glfwApplication{m, "Application", "GLFW application"}; + py::class_> glfwApplication{m, "Application", "GLFW application"}; /** @todo def_property_writeonly for swap_interval */ PyNonDestructibleClass inputEvent_{glfwApplication, "InputEvent", "Base for input events"}; diff --git a/src/python/magnum/platform/glx.cpp b/src/python/magnum/platform/glx.cpp index b6cc5b4..554ed85 100644 --- a/src/python/magnum/platform/glx.cpp +++ b/src/python/magnum/platform/glx.cpp @@ -28,6 +28,7 @@ #include "magnum/bootstrap.h" #include "magnum/platform/windowlessapplication.h" +#include "magnum/platform/holder.h" namespace magnum { namespace platform { @@ -64,7 +65,7 @@ void glx(py::module_& m) { virtual ~PyWindowlessApplication() {} }; - py::class_ windowlessGlxApplication{m, "WindowlessApplication", "Windowless GLX application"}; + py::class_> windowlessGlxApplication{m, "WindowlessApplication", "Windowless GLX application"}; windowlessapplication(windowlessGlxApplication); } diff --git a/src/python/magnum/platform/holder.h b/src/python/magnum/platform/holder.h new file mode 100644 index 0000000..837a59c --- /dev/null +++ b/src/python/magnum/platform/holder.h @@ -0,0 +1,72 @@ +#ifndef magnum_platform_holder_h +#define magnum_platform_holder_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +namespace magnum { namespace platform { namespace { + +/* Takes care of updating magnum::glContextOwner so it doesn't need to be + duplicated in every application implementation */ +template struct ApplicationHolder: std::unique_ptr { + explicit ApplicationHolder(T* object): std::unique_ptr{object} { + /* There's no real possibility to export a symbol from magnum.gl and + access it from here (because there's no real possibility for a + module to ensure another module is loaded before it in order to make + the symbols resolve correctly; Corrade's PluginManager does that by + having dependency info *external* to the module, that's the only + way), so we're sharing the data using a bunch of very ugly + allocations instead. Fortunately construction/destruction of an + application happens *very seldom*, and gl.Context.current() + hopefully also not that often. Yes, the parameter is a std::string. + JOY. */ + auto* glContextOwner = static_cast*>(py::get_shared_data("magnumGLContextOwner")); + if(!glContextOwner) + py::set_shared_data("magnumGLContextOwner", glContextOwner = new std::pair{}); + + CORRADE_ASSERT(!glContextOwner->first, "Sorry, just one magnum.*.Application instance can exist at a time", ); + *glContextOwner = {object, &typeid(T)}; + } + + ApplicationHolder(ApplicationHolder&&) noexcept = default; + ApplicationHolder(const ApplicationHolder&) = delete; + + ~ApplicationHolder() { + auto* glContextOwner = static_cast*>(py::get_shared_data("magnumGLContextOwner")); + CORRADE_INTERNAL_ASSERT(glContextOwner && glContextOwner->first == this->get()); + delete glContextOwner; + py::set_shared_data("magnumGLContextOwner", nullptr); + } + + ApplicationHolder& operator=(ApplicationHolder&&) noexcept = default; + ApplicationHolder& operator=(const ApplicationHolder&) = default; +}; + +}}} + +PYBIND11_DECLARE_HOLDER_TYPE(T, magnum::platform::ApplicationHolder) + +#endif diff --git a/src/python/magnum/platform/sdl2.cpp b/src/python/magnum/platform/sdl2.cpp index bb83795..70d159b 100644 --- a/src/python/magnum/platform/sdl2.cpp +++ b/src/python/magnum/platform/sdl2.cpp @@ -30,6 +30,7 @@ #include "magnum/bootstrap.h" #include "magnum/platform/application.h" +#include "magnum/platform/holder.h" namespace magnum { namespace platform { @@ -155,7 +156,7 @@ void sdl2(py::module_& m) { } }; - py::class_ sdl2application{m, "Application", "SDL2 application"}; + py::class_> sdl2application{m, "Application", "SDL2 application"}; sdl2application .def_property("swap_interval", &PyApplication::swapInterval, [](PublicizedApplication& self, Int interval) { diff --git a/src/python/magnum/platform/wgl.cpp b/src/python/magnum/platform/wgl.cpp index e00ca85..63ec318 100644 --- a/src/python/magnum/platform/wgl.cpp +++ b/src/python/magnum/platform/wgl.cpp @@ -28,6 +28,7 @@ #include "magnum/bootstrap.h" #include "magnum/platform/windowlessapplication.h" +#include "magnum/platform/holder.h" namespace magnum { namespace platform { @@ -64,7 +65,7 @@ void wgl(py::module_& m) { virtual ~PyWindowlessApplication() {} }; - py::class_ windowlessWglApplication{m, "WindowlessApplication", "Windowless WGL application"}; + py::class_> windowlessWglApplication{m, "WindowlessApplication", "Windowless WGL application"}; windowlessapplication(windowlessWglApplication); } diff --git a/src/python/magnum/platform/windowlessapplication.h b/src/python/magnum/platform/windowlessapplication.h index e130147..8bc9208 100644 --- a/src/python/magnum/platform/windowlessapplication.h +++ b/src/python/magnum/platform/windowlessapplication.h @@ -29,7 +29,7 @@ namespace magnum { namespace platform { -template void windowlessapplication(py::class_& c) { +template void windowlessapplication(py::class_& c) { py::class_ configuration{c, "Configuration", "Configuration"}; configuration .def(py::init()); diff --git a/src/python/magnum/test/test_gl.py b/src/python/magnum/test/test_gl.py index 3c9bc58..4f4f3af 100644 --- a/src/python/magnum/test/test_gl.py +++ b/src/python/magnum/test/test_gl.py @@ -36,6 +36,13 @@ class Attribute(unittest.TestCase): self.assertEqual(a.components, gl.Attribute.Components.TWO) self.assertEqual(a.data_type, gl.Attribute.DataType.FLOAT) +class Context(unittest.TestCase): + def test_no_current(self): + self.assertFalse(gl.Context.has_current) + + with self.assertRaisesRegex(RuntimeError, "no current context"): + gl.Context.current + class FramebufferClear(unittest.TestCase): def test_ops(self): self.assertEqual(gl.FramebufferClear.COLOR|gl.FramebufferClear.COLOR, gl.FramebufferClear.COLOR) diff --git a/src/python/magnum/test/test_gl_gl.py b/src/python/magnum/test/test_gl_gl.py index 55d80a1..b324ac6 100644 --- a/src/python/magnum/test/test_gl_gl.py +++ b/src/python/magnum/test/test_gl_gl.py @@ -132,6 +132,29 @@ class Buffer(GLTestCase): a = gl.Buffer() a.set_data(array.array('f', [0.5, 1.2])) +class Context(GLTestCase): + def test(self): + self.assertTrue(gl.Context.has_current) + + # Retrieving the context should hold down the app with one more + # reference + app_refcount = sys.getrefcount(self.app) + current = gl.Context.current + self.assertEqual(sys.getrefcount(self.app), app_refcount + 1) + self.assertEqual(current.owner, self.app) + + # Some properties + self.assertGreater(len(current.renderer_string), 0) + self.assertGreater(len(current.extension_strings), 0) + + # Interestingly enough, this "just works". I thought I would need to do + # something extra but apparently not + current2 = gl.Context.current + self.assertTrue(id(current) == id(current2)) + + del current, current2 + self.assertEqual(sys.getrefcount(self.app), app_refcount) + class DefaultFramebuffer(GLTestCase): def test(self): # Using it should not crash, leak or cause double-free issues