├── .gitignore ├── LICENSE ├── README.md ├── setup.cfg └── xr_examples ├── __init__.py ├── api_dump_layer.py ├── color_cube.py ├── debug_all_the_things.py ├── extension_properties.py ├── gl_example.py ├── green_blue.py ├── headless.py ├── hello_xr.py ├── hello_xr ├── __init__.py ├── geometry.py ├── graphics_plugin.py ├── graphics_plugin_opengl.py ├── linear.py ├── main.py ├── openxr_program.py ├── options.py ├── platform_plugin.py ├── platform_plugin_win32.py └── platform_plugin_xlib.py ├── openxr_version.py ├── pink_world.py ├── runtime_name.py ├── track_controller.py ├── track_hmd.py ├── track_hmd2.py ├── vive_tracker.py └── where_does_it_hang.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm 132 | /.idea 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyopenxr_examples 2 | 3 | Sample programs using the pyopenxr python AR/VR bindings from https://github.com/cmbruns/pyopenxr 4 | 5 | 1. `hello_xr.py`: This is a faithful translation into python of the hello_xr example from https://github.com/KhronosGroup/OpenXR-SDK-Source/tree/main/src/tests/hello_xr . Watch the colorful cubes attached to various spatial frames, and squeeze the controller triggers to experience an effect. This is a multi-source-file example, because the original C source also spans multiple files. hello_xr screen shot 6 | 2. `color_cube.py`: This is a one-file example showing a large cube on the floor in the center of your VR space. This example uses a high-level `xr.ContextObject` instance to help keep the example compact. color_cube.py screen shot 7 | 3. `debug_all_the_things.py`: Combines seven different ways to add logging messages to help debug `pyopenxr` programs. 8 | 4. `pink_world.py`: The simplest opengl rendering example. The whole universe is a uniform pink. 9 | 5. `track_hmd2.py` : Prints the location of the headset to the console. 10 | 6. `track_controller.py`: Prints the location of the motion controllers to the console. 11 | 7. `vive_tracker.py` : Print the locations of Vive Tracker devices. But not if they are assigned "handheld_object" role for some reason. 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyopenxr_examples 3 | version = 0.5 4 | author = Christopher M. Bruns and others 5 | author_email = cmbruns@rotatingpenguin.com 6 | description = Example programs using pyopenxr VR/AR API 7 | url = https://github.com/cmbruns/pyopenxr_examples 8 | project_urls = 9 | Bug Tracker = https://github.com/cmbruns/pyopenxr_examples/issues 10 | classifiers = 11 | Intended Audience :: Developers 12 | Intended Audience :: Science/Research 13 | License :: OSI Approved :: Apache Software License 14 | Operating System :: Microsoft :: Windows 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python :: 3 :: Only 17 | Programming Language :: Python :: Implementation :: CPython 18 | Topic :: Multimedia :: Graphics :: 3D Rendering 19 | Topic :: Scientific/Engineering :: Visualization 20 | Development Status :: 4 - Beta 21 | 22 | [options.entry_points] 23 | console_scripts = 24 | hello_xr = xr_examples:hello_xr:main 25 | 26 | [options] 27 | install_requires = 28 | pyopenxr 29 | numpy 30 | PyOpenGL 31 | glfw 32 | package_dir = 33 | =. 34 | packages = xr_examples 35 | ; 3.5 does not support f-strings 36 | python_requires = >=3.6 37 | -------------------------------------------------------------------------------- /xr_examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmbruns/pyopenxr_examples/9e3817c8bfd0bcd66282161e014f5256cf123979/xr_examples/__init__.py -------------------------------------------------------------------------------- /xr_examples/api_dump_layer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import xr 4 | 5 | # Turn on extra debugging messages in the OpenXR Loader 6 | # os.environ["XR_LOADER_DEBUG"] = "all" 7 | # os.environ["LD_BIND_NOW"] = "1" 8 | 9 | print(os.environ["XR_API_LAYER_PATH"]) 10 | print(len(xr.enumerate_api_layer_properties())) 11 | print(*xr.enumerate_api_layer_properties()) 12 | 13 | # 1) Setting/clearing the environment from python does affect the layers 14 | os.environ.pop("XR_API_LAYER_PATH", None) 15 | print(len(xr.enumerate_api_layer_properties())) 16 | print(*xr.enumerate_api_layer_properties()) 17 | 18 | xr.expose_packaged_api_layers() 19 | print(len(xr.enumerate_api_layer_properties())) 20 | print(*xr.enumerate_api_layer_properties()) 21 | 22 | foo_layer = xr.SteamVrLinuxDestroyInstanceLayer() 23 | print(foo_layer.name) 24 | 25 | # 2) The choice of layers can be specified in the instance constructor 26 | instance_handle = xr.create_instance(create_info=xr.InstanceCreateInfo( 27 | enabled_api_layer_names=[ 28 | xr.LUNARG_api_dump_APILAYER_NAME, 29 | foo_layer.name, 30 | # xr.LUNARG_core_validation_APILAYER_NAME, 31 | # xr.LUNARG_api_dump_APILAYER_NAME, 32 | ],)) 33 | 34 | xr.destroy_instance(instance_handle) 35 | -------------------------------------------------------------------------------- /xr_examples/color_cube.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program color_cube.py 3 | This example renders one big cube. 4 | """ 5 | 6 | import inspect 7 | from OpenGL import GL 8 | from OpenGL.GL.shaders import compileShader, compileProgram 9 | import xr 10 | 11 | 12 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 13 | with xr.ContextObject( 14 | instance_create_info=xr.InstanceCreateInfo( 15 | enabled_extension_names=[ 16 | # A graphics extension is mandatory (without a headless extension) 17 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 18 | ], 19 | ), 20 | ) as context: 21 | vertex_shader = compileShader( 22 | inspect.cleandoc(""" 23 | #version 430 24 | 25 | // Adapted from @jherico's RiftDemo.py in pyovr 26 | 27 | /* Draws a cube: 28 | 29 | 2________ 3 30 | /| /| 31 | 6/_|____7/ | 32 | | |_____|_| 33 | | /0 | /1 34 | |/______|/ 35 | 4 5 36 | 37 | */ 38 | 39 | layout(location = 0) uniform mat4 Projection = mat4(1); 40 | layout(location = 4) uniform mat4 ModelView = mat4(1); 41 | layout(location = 8) uniform float Size = 0.3; 42 | 43 | // Minimum Y value is zero, so cube sits on the floor in room scale 44 | const vec3 UNIT_CUBE[8] = vec3[8]( 45 | vec3(-1.0, -0.0, -1.0), // 0: lower left rear 46 | vec3(+1.0, -0.0, -1.0), // 1: lower right rear 47 | vec3(-1.0, +2.0, -1.0), // 2: upper left rear 48 | vec3(+1.0, +2.0, -1.0), // 3: upper right rear 49 | vec3(-1.0, -0.0, +1.0), // 4: lower left front 50 | vec3(+1.0, -0.0, +1.0), // 5: lower right front 51 | vec3(-1.0, +2.0, +1.0), // 6: upper left front 52 | vec3(+1.0, +2.0, +1.0) // 7: upper right front 53 | ); 54 | 55 | const vec3 UNIT_CUBE_NORMALS[6] = vec3[6]( 56 | vec3(0.0, 0.0, -1.0), 57 | vec3(0.0, 0.0, 1.0), 58 | vec3(1.0, 0.0, 0.0), 59 | vec3(-1.0, 0.0, 0.0), 60 | vec3(0.0, 1.0, 0.0), 61 | vec3(0.0, -1.0, 0.0) 62 | ); 63 | 64 | const int CUBE_INDICES[36] = int[36]( 65 | 0, 1, 2, 2, 1, 3, // rear 66 | 4, 6, 5, 6, 7, 5, // front 67 | 0, 2, 4, 4, 2, 6, // left 68 | 1, 3, 5, 5, 3, 7, // right 69 | 2, 6, 3, 6, 3, 7, // top 70 | 0, 1, 4, 4, 1, 5 // bottom 71 | ); 72 | 73 | out vec3 _color; 74 | 75 | void main() { 76 | _color = vec3(1.0, 0.0, 0.0); 77 | int vertexIndex = CUBE_INDICES[gl_VertexID]; 78 | int normalIndex = gl_VertexID / 6; 79 | 80 | _color = UNIT_CUBE_NORMALS[normalIndex]; 81 | if (any(lessThan(_color, vec3(0.0)))) { 82 | _color = vec3(1.0) + _color; 83 | } 84 | 85 | gl_Position = Projection * ModelView * vec4(UNIT_CUBE[vertexIndex] * Size, 1.0); 86 | } 87 | """), GL.GL_VERTEX_SHADER) 88 | fragment_shader = compileShader( 89 | inspect.cleandoc(""" 90 | #version 430 91 | 92 | in vec3 _color; 93 | out vec4 FragColor; 94 | 95 | void main() { 96 | FragColor = vec4(_color, 1.0); 97 | } 98 | """), GL.GL_FRAGMENT_SHADER) 99 | shader = compileProgram(vertex_shader, fragment_shader) 100 | vao = GL.glGenVertexArrays(1) 101 | GL.glBindVertexArray(vao) 102 | GL.glEnable(GL.GL_DEPTH_TEST) 103 | GL.glClearColor(0.2, 0.2, 0.2, 1) 104 | GL.glClearDepth(1.0) 105 | for frame_index, frame_state in enumerate(context.frame_loop()): 106 | for view_index, view in enumerate(context.view_loop(frame_state)): 107 | if view_index == 1: 108 | # continue 109 | pass 110 | # print(view_index, view.pose.position) # Are both eyes the same? 111 | projection = xr.Matrix4x4f.create_projection_fov( 112 | graphics_api=xr.GraphicsAPI.OPENGL, 113 | fov=view.fov, 114 | near_z=0.05, 115 | far_z=100.0, # tip: use negative far_z for infinity projection... 116 | ) 117 | to_view = xr.Matrix4x4f.create_translation_rotation_scale( 118 | translation=view.pose.position, 119 | rotation=view.pose.orientation, 120 | scale=(1, 1, 1), 121 | ) 122 | view = xr.Matrix4x4f.invert_rigid_body(to_view) 123 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 124 | GL.glUseProgram(shader) 125 | GL.glUniformMatrix4fv(0, 1, False, projection.as_numpy()) 126 | GL.glUniformMatrix4fv(4, 1, False, view.as_numpy()) 127 | GL.glBindVertexArray(vao) 128 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, 36) 129 | # if frame_index > 3: break 130 | -------------------------------------------------------------------------------- /xr_examples/debug_all_the_things.py: -------------------------------------------------------------------------------- 1 | """ 2 | debug_all_the_things.py 3 | 4 | This example program demonstrates several logging aids for debugging pyopenxr programs. 5 | Ordinarily you would not do all these things at once. 6 | """ 7 | 8 | import ctypes 9 | import logging # 1) Use the python logging system 10 | import os 11 | 12 | import glfw 13 | from OpenGL import GL # OpenGL can report debug messages too 14 | import xr 15 | 16 | # 2) Hook into the pyopenxr logger hierarchy. 17 | logging.basicConfig() # You might want also to study the parameters to basicConfig()... 18 | pyopenxr_logger = logging.getLogger("pyopenxr") 19 | # show me ALL the messages 20 | pyopenxr_logger.setLevel(logging.DEBUG) # Modify argument according to your current needs 21 | 22 | # 3) Create a child logger for this particular program 23 | logger = logging.getLogger("pyopenxr.debug_all_the_things") 24 | logger.info("Hey, this logging thing works!") # Test it out 25 | 26 | # Use API layers for debugging 27 | enabled_api_layers = [] 28 | 29 | # 4) Core validation adds additional messages about correct use of the OpenXR api 30 | if xr.LUNARG_core_validation_APILAYER_NAME in xr.enumerate_api_layer_properties(): 31 | enabled_api_layers.append(xr.LUNARG_core_validation_APILAYER_NAME) 32 | 33 | # 5) API dump shows the details of every OpenXR call. Use this for deep debugging only. 34 | if xr.LUNARG_api_dump_APILAYER_NAME in xr.enumerate_api_layer_properties(): 35 | enabled_api_layers.append(xr.LUNARG_api_dump_APILAYER_NAME) 36 | os.environ["XR_API_DUMP_EXPORT_TYPE"] = "text" # or "html" 37 | # os.environ["XR_API_DUMP_FILE_NAME"] = "/some/file/name" 38 | 39 | # Use extensions for debugging 40 | enabled_extensions = [] 41 | # 6) XR_EXT_debug_utils can be used to redirect pyopenxr debugging messages to our logger, 42 | # among other things. 43 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties(): 44 | enabled_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME) 45 | 46 | 47 | # Define helper function for our logging callback 48 | def openxr_log_level(severity_flags: xr.DebugUtilsMessageSeverityFlagsEXT) -> int: 49 | """Convert OpenXR message severities to python logging severities.""" 50 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: 51 | return logging.ERROR 52 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: 53 | return logging.WARNING 54 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: 55 | return logging.INFO 56 | else: 57 | return logging.DEBUG 58 | 59 | 60 | def xr_debug_callback( 61 | severity: xr.DebugUtilsMessageSeverityFlagsEXT, 62 | _type: xr.DebugUtilsMessageTypeFlagsEXT, 63 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT), 64 | _user_data: ctypes.c_void_p) -> bool: 65 | """Redirect OpenXR messages to our python logger.""" 66 | d = data.contents 67 | pyopenxr_logger.log( 68 | level=openxr_log_level(severity), 69 | msg=f"OpenXR: {d.function_name.decode()}: {d.message.decode()}") 70 | return True 71 | 72 | 73 | # Prepare to create a debug utils messenger 74 | pfn_xr_debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback) 75 | debug_utils_messenger_create_info = xr.DebugUtilsMessengerCreateInfoEXT( 76 | message_severities=( 77 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 78 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 79 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT 80 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT 81 | ), 82 | message_types=( 83 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 84 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 85 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 86 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT 87 | ), 88 | user_callback=pfn_xr_debug_callback, 89 | ) 90 | 91 | ptr_debug_messenger = None 92 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties(): 93 | # Insert our debug_utils_messenger create_info 94 | ptr_debug_messenger = ctypes.cast( 95 | ctypes.pointer(debug_utils_messenger_create_info), ctypes.c_void_p) 96 | 97 | # 7) Turn on extra debugging messages in the OpenXR Loader 98 | os.environ["XR_LOADER_DEBUG"] = "all" 99 | os.environ["LD_BIND_NOW"] = "1" 100 | 101 | 102 | # Create OpenXR instance with attached layers, extensions, and debug messenger. 103 | instance = xr.create_instance( 104 | xr.InstanceCreateInfo( 105 | enabled_api_layer_names=enabled_api_layers, 106 | enabled_extension_names=enabled_extensions, 107 | next=ptr_debug_messenger, 108 | ) 109 | ) 110 | 111 | # OpenGL can report debug messages too 112 | # Create a sub-logger for messages from OpenGL 113 | gl_logger = logging.getLogger("pyopenxr.opengl") 114 | 115 | 116 | def gl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user): 117 | """Redirect OpenGL debug messages""" 118 | log_level = { 119 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR, 120 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING, 121 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO, 122 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG, 123 | }[severity] 124 | gl_logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}") 125 | 126 | 127 | gl_debug_message_proc = GL.GLDEBUGPROC(gl_debug_message_callback) 128 | # We need an active OpenGL context before calling glDebugMessageCallback 129 | glfw.init() 130 | window = glfw.create_window(16, 16, "glfw window", None, None) 131 | glfw.make_context_current(window) 132 | GL.glDebugMessageCallback(gl_debug_message_proc, None) 133 | 134 | 135 | # Clean up 136 | glfw.terminate() 137 | xr.destroy_instance(instance) 138 | -------------------------------------------------------------------------------- /xr_examples/extension_properties.py: -------------------------------------------------------------------------------- 1 | import xr 2 | 3 | # Query the available VR/AR extensions 4 | available = xr.enumerate_instance_extension_properties() 5 | for prop in available: 6 | print(prop) 7 | 8 | # Replace with whatever extensions are required for your 9 | # particular application... 10 | required = [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, ] 11 | for prop in required: 12 | assert prop in available 13 | -------------------------------------------------------------------------------- /xr_examples/gl_example.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import logging 3 | 4 | import glfw 5 | import platform 6 | from OpenGL import GL 7 | if platform.system() == "Windows": 8 | from OpenGL import WGL 9 | elif platform.system() == "Linux": 10 | from OpenGL import GLX 11 | 12 | import xr 13 | 14 | 15 | ALL_SEVERITIES = ( 16 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 17 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 18 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT 19 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT 20 | ) 21 | 22 | ALL_TYPES = ( 23 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 24 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 25 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 26 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT 27 | ) 28 | 29 | 30 | def py_log_level(severity_flags: int): 31 | if severity_flags & 0x0001: # VERBOSE 32 | return logging.DEBUG 33 | if severity_flags & 0x0010: # INFO 34 | return logging.INFO 35 | if severity_flags & 0x0100: # WARNING 36 | return logging.WARNING 37 | if severity_flags & 0x1000: # ERROR 38 | return logging.ERROR 39 | return logging.CRITICAL 40 | 41 | 42 | stringForFormat = { 43 | GL.GL_COMPRESSED_R11_EAC: "COMPRESSED_R11_EAC", 44 | GL.GL_COMPRESSED_RED_RGTC1: "COMPRESSED_RED_RGTC1", 45 | GL.GL_COMPRESSED_RG_RGTC2: "COMPRESSED_RG_RGTC2", 46 | GL.GL_COMPRESSED_RG11_EAC: "COMPRESSED_RG11_EAC", 47 | GL.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: "COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT", 48 | GL.GL_COMPRESSED_RGB8_ETC2: "COMPRESSED_RGB8_ETC2", 49 | GL.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: "COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2", 50 | GL.GL_COMPRESSED_RGBA8_ETC2_EAC: "COMPRESSED_RGBA8_ETC2_EAC", 51 | GL.GL_COMPRESSED_SIGNED_R11_EAC: "COMPRESSED_SIGNED_R11_EAC", 52 | GL.GL_COMPRESSED_SIGNED_RG11_EAC: "COMPRESSED_SIGNED_RG11_EAC", 53 | GL.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: "COMPRESSED_SRGB_ALPHA_BPTC_UNORM", 54 | GL.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: "COMPRESSED_SRGB8_ALPHA8_ETC2_EAC", 55 | GL.GL_COMPRESSED_SRGB8_ETC2: "COMPRESSED_SRGB8_ETC2", 56 | GL.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: "COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2", 57 | GL.GL_DEPTH_COMPONENT16: "DEPTH_COMPONENT16", 58 | GL.GL_DEPTH_COMPONENT24: "DEPTH_COMPONENT24", 59 | GL.GL_DEPTH_COMPONENT32: "DEPTH_COMPONENT32", 60 | GL.GL_DEPTH_COMPONENT32F: "DEPTH_COMPONENT32F", 61 | GL.GL_DEPTH24_STENCIL8: "DEPTH24_STENCIL8", 62 | GL.GL_R11F_G11F_B10F: "R11F_G11F_B10F", 63 | GL.GL_R16_SNORM: "R16_SNORM", 64 | GL.GL_R16: "R16", 65 | GL.GL_R16F: "R16F", 66 | GL.GL_R16I: "R16I", 67 | GL.GL_R16UI: "R16UI", 68 | GL.GL_R32F: "R32F", 69 | GL.GL_R32I: "R32I", 70 | GL.GL_R32UI: "R32UI", 71 | GL.GL_R8_SNORM: "R8_SNORM", 72 | GL.GL_R8: "R8", 73 | GL.GL_R8I: "R8I", 74 | GL.GL_R8UI: "R8UI", 75 | GL.GL_RG16_SNORM: "RG16_SNORM", 76 | GL.GL_RG16: "RG16", 77 | GL.GL_RG16F: "RG16F", 78 | GL.GL_RG16I: "RG16I", 79 | GL.GL_RG16UI: "RG16UI", 80 | GL.GL_RG32F: "RG32F", 81 | GL.GL_RG32I: "RG32I", 82 | GL.GL_RG32UI: "RG32UI", 83 | GL.GL_RG8_SNORM: "RG8_SNORM", 84 | GL.GL_RG8: "RG8", 85 | GL.GL_RG8I: "RG8I", 86 | GL.GL_RG8UI: "RG8UI", 87 | GL.GL_RGB10_A2: "RGB10_A2", 88 | GL.GL_RGB8: "RGB8", 89 | GL.GL_RGB9_E5: "RGB9_E5", 90 | GL.GL_RGBA16_SNORM: "RGBA16_SNORM", 91 | GL.GL_RGBA16: "RGBA16", 92 | GL.GL_RGBA16F: "RGBA16F", 93 | GL.GL_RGBA16I: "RGBA16I", 94 | GL.GL_RGBA16UI: "RGBA16UI", 95 | GL.GL_RGBA2: "RGBA2", 96 | GL.GL_RGBA32F: "RGBA32F", 97 | GL.GL_RGBA32I: "RGBA32I", 98 | GL.GL_RGBA32UI: "RGBA32UI", 99 | GL.GL_RGBA8_SNORM: "RGBA8_SNORM", 100 | GL.GL_RGBA8: "RGBA8", 101 | GL.GL_RGBA8I: "RGBA8I", 102 | GL.GL_RGBA8UI: "RGBA8UI", 103 | GL.GL_SRGB8_ALPHA8: "SRGB8_ALPHA8", 104 | GL.GL_SRGB8: "SRGB8", 105 | GL.GL_RGB16F: "RGB16F", 106 | GL.GL_DEPTH32F_STENCIL8: "DEPTH32F_STENCIL8", 107 | GL.GL_BGR: "BGR (Out of spec)", 108 | GL.GL_BGRA: "BGRA (Out of spec)", 109 | } 110 | 111 | 112 | class OpenXrExample(object): 113 | def __init__(self, log_level=logging.WARNING): 114 | logging.basicConfig() 115 | self.logger = logging.getLogger("gl_example") 116 | self.logger.setLevel(log_level) 117 | self.debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(self.debug_callback_py) 118 | self.mirror_window = False 119 | self.instance = None 120 | self.system_id = None 121 | self.pxrCreateDebugUtilsMessengerEXT = None 122 | self.pxrDestroyDebugUtilsMessengerEXT = None 123 | self.pxrGetOpenGLGraphicsRequirementsKHR = None 124 | self.graphics_requirements = xr.GraphicsRequirementsOpenGLKHR() 125 | if platform.system() == 'Windows': 126 | self.graphics_binding = xr.GraphicsBindingOpenGLWin32KHR() 127 | elif platform.system() == 'Linux': 128 | self.graphics_binding = xr.GraphicsBindingOpenGLXlibKHR() 129 | else: 130 | raise NotImplementedError('Unsupported platform') 131 | self.render_target_size = None 132 | self.window = None 133 | self.session = None 134 | self.projection_layer_views = (xr.CompositionLayerProjectionView * 2)( 135 | *([xr.CompositionLayerProjectionView()] * 2)) 136 | self.projection_layer = xr.CompositionLayerProjection( 137 | views=self.projection_layer_views) 138 | self.swapchain_create_info = xr.SwapchainCreateInfo() 139 | self.swapchain = None 140 | self.swapchain_images = None 141 | self.fbo_id = None 142 | self.fbo_depth_buffer = None 143 | self.quit = False 144 | self.session_state = xr.SessionState.IDLE 145 | self.frame_state = xr.FrameState() 146 | self.eye_view_states = None 147 | self.window_size = None 148 | self.enable_debug = True 149 | self.linux_steamvr_broken_destroy_instance = False 150 | 151 | def debug_callback_py( 152 | self, 153 | severity: xr.DebugUtilsMessageSeverityFlagsEXT, 154 | _type: xr.DebugUtilsMessageTypeFlagsEXT, 155 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT), 156 | _user_data: ctypes.c_void_p, 157 | ) -> bool: 158 | d = data.contents 159 | # TODO structure properties to return unicode strings 160 | self.logger.log(py_log_level(severity), f"{d.function_name.decode()}: {d.message.decode()}") 161 | return True 162 | 163 | def run(self): 164 | while not self.quit: 165 | if glfw.window_should_close(self.window): 166 | self.quit = True 167 | else: 168 | self.frame() 169 | 170 | def __enter__(self): 171 | self.prepare_xr_instance() 172 | self.prepare_xr_system() 173 | self.prepare_window() 174 | self.prepare_xr_session() 175 | self.prepare_xr_swapchain() 176 | self.prepare_xr_composition_layers() 177 | self.prepare_gl_framebuffer() 178 | return self 179 | 180 | def prepare_xr_instance(self): 181 | discovered_extensions = xr.enumerate_instance_extension_properties() 182 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME not in discovered_extensions: 183 | self.enable_debug = False 184 | requested_extensions = [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME] 185 | if self.enable_debug: 186 | requested_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME) 187 | for extension in requested_extensions: 188 | assert extension in discovered_extensions 189 | app_info = xr.ApplicationInfo( 190 | application_name="gl_example", 191 | application_version=0, 192 | engine_name="pyopenxr", 193 | engine_version=xr.PYOPENXR_CURRENT_API_VERSION, 194 | api_version=xr.Version(1, 0, xr.XR_VERSION_PATCH), 195 | ) 196 | ici = xr.InstanceCreateInfo( 197 | application_info=app_info, 198 | enabled_extension_names=requested_extensions, 199 | ) 200 | dumci = xr.DebugUtilsMessengerCreateInfoEXT() 201 | if self.enable_debug: 202 | dumci.message_severities = ALL_SEVERITIES 203 | dumci.message_types = ALL_TYPES 204 | dumci.user_data = None # TODO 205 | dumci.user_callback = self.debug_callback 206 | ici.next = ctypes.cast(ctypes.pointer(dumci), ctypes.c_void_p) # TODO: yuck 207 | self.instance = xr.create_instance(ici) 208 | # TODO: pythonic wrapper 209 | self.pxrGetOpenGLGraphicsRequirementsKHR = ctypes.cast( 210 | xr.get_instance_proc_addr( 211 | self.instance, 212 | "xrGetOpenGLGraphicsRequirementsKHR", 213 | ), 214 | xr.PFN_xrGetOpenGLGraphicsRequirementsKHR 215 | ) 216 | instance_props = xr.get_instance_properties(self.instance) 217 | if platform.system() == 'Linux' and instance_props.runtime_name == b"SteamVR/OpenXR": 218 | print("SteamVR/OpenXR on Linux detected, enabling workarounds") 219 | # Enabling workaround for https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422, 220 | # and https://github.com/ValveSoftware/SteamVR-for-Linux/issues/479 221 | # destroy_instance() causes SteamVR to hang and never recover 222 | self.linux_steamvr_broken_destroy_instance = True 223 | 224 | def prepare_xr_system(self): 225 | get_info = xr.SystemGetInfo(xr.FormFactor.HEAD_MOUNTED_DISPLAY) 226 | self.system_id = xr.get_system(self.instance, get_info) # TODO: not a pointer 227 | view_configs = xr.enumerate_view_configurations(self.instance, self.system_id) 228 | assert view_configs[0] == xr.ViewConfigurationType.PRIMARY_STEREO.value # TODO: equality... 229 | view_config_views = xr.enumerate_view_configuration_views( 230 | self.instance, self.system_id, xr.ViewConfigurationType.PRIMARY_STEREO) 231 | assert len(view_config_views) == 2 232 | assert view_config_views[0].recommended_image_rect_height == view_config_views[1].recommended_image_rect_height 233 | self.render_target_size = ( 234 | view_config_views[0].recommended_image_rect_width * 2, 235 | view_config_views[0].recommended_image_rect_height) 236 | result = self.pxrGetOpenGLGraphicsRequirementsKHR( 237 | self.instance, self.system_id, ctypes.byref(self.graphics_requirements)) # TODO: pythonic wrapper 238 | result = xr.exception.check_result(xr.Result(result)) 239 | if result.is_exception(): 240 | raise result 241 | 242 | def prepare_window(self): 243 | if not glfw.init(): 244 | raise RuntimeError("GLFW initialization failed") 245 | if not self.mirror_window: 246 | glfw.window_hint(glfw.VISIBLE, False) 247 | glfw.window_hint(glfw.DOUBLEBUFFER, False) 248 | glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4) 249 | glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 5) 250 | glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) 251 | self.window_size = [s // 4 for s in self.render_target_size] 252 | self.window = glfw.create_window(*self.window_size, "gl_example", None, None) 253 | if self.window is None: 254 | raise RuntimeError("Failed to create GLFW window") 255 | glfw.make_context_current(self.window) 256 | # Attempt to disable vsync on the desktop window or 257 | # it will interfere with the OpenXR frame loop timing 258 | glfw.swap_interval(0) 259 | 260 | def prepare_xr_session(self): 261 | if platform.system() == 'Windows': 262 | self.graphics_binding.h_dc = WGL.wglGetCurrentDC() 263 | self.graphics_binding.h_glrc = WGL.wglGetCurrentContext() 264 | else: 265 | self.graphics_binding.x_display = GLX.glXGetCurrentDisplay() 266 | self.graphics_binding.glx_context = GLX.glXGetCurrentContext() 267 | self.graphics_binding.glx_drawable = GLX.glXGetCurrentDrawable() 268 | pp = ctypes.cast(ctypes.pointer(self.graphics_binding), ctypes.c_void_p) 269 | sci = xr.SessionCreateInfo(0, self.system_id, next=pp) 270 | self.session = xr.create_session(self.instance, sci) 271 | reference_spaces = xr.enumerate_reference_spaces(self.session) 272 | for rs in reference_spaces: 273 | self.logger.debug(f"Session supports reference space {xr.ReferenceSpaceType(rs)}") 274 | # TODO: default constructors for Quaternion, Vector3f, Posef, ReferenceSpaceCreateInfo 275 | rsci = xr.ReferenceSpaceCreateInfo( 276 | xr.ReferenceSpaceType.STAGE, 277 | xr.Posef(xr.Quaternionf(0, 0, 0, 1), xr.Vector3f(0, 0, 0)) 278 | ) 279 | self.projection_layer.space = xr.create_reference_space(self.session, rsci) 280 | swapchain_formats = xr.enumerate_swapchain_formats(self.session) 281 | for scf in swapchain_formats: 282 | self.logger.debug(f"Session supports swapchain format {stringForFormat[scf]}") 283 | 284 | def prepare_xr_swapchain(self): 285 | self.swapchain_create_info.usage_flags = xr.SWAPCHAIN_USAGE_TRANSFER_DST_BIT 286 | self.swapchain_create_info.format = GL.GL_SRGB8_ALPHA8 287 | self.swapchain_create_info.sample_count = 1 288 | self.swapchain_create_info.array_size = 1 289 | self.swapchain_create_info.face_count = 1 290 | self.swapchain_create_info.mip_count = 1 291 | self.swapchain_create_info.width = self.render_target_size[0] 292 | self.swapchain_create_info.height = self.render_target_size[1] 293 | self.swapchain = xr.create_swapchain(self.session, self.swapchain_create_info) 294 | self.swapchain_images = xr.enumerate_swapchain_images(self.swapchain, xr.SwapchainImageOpenGLKHR) 295 | for i, si in enumerate(self.swapchain_images): 296 | self.logger.debug(f"Swapchain image {i} type = {xr.StructureType(si.type)}") 297 | 298 | def prepare_xr_composition_layers(self): 299 | self.projection_layer.view_count = 2 300 | self.projection_layer.views = self.projection_layer_views 301 | for eye_index in range(2): 302 | layer_view = self.projection_layer_views[eye_index] 303 | layer_view.sub_image.swapchain = self.swapchain 304 | layer_view.sub_image.image_rect.extent = xr.Extent2Di( 305 | self.render_target_size[0] // 2, 306 | self.render_target_size[1], 307 | ) 308 | if eye_index == 1: 309 | layer_view.sub_image.image_rect.offset.x = layer_view.sub_image.image_rect.extent.width 310 | 311 | def prepare_gl_framebuffer(self): 312 | glfw.make_context_current(self.window) 313 | self.fbo_depth_buffer = GL.glGenRenderbuffers(1) 314 | GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, self.fbo_depth_buffer) 315 | if self.swapchain_create_info.sample_count == 1: 316 | GL.glRenderbufferStorage( 317 | GL.GL_RENDERBUFFER, 318 | GL.GL_DEPTH24_STENCIL8, 319 | self.swapchain_create_info.width, 320 | self.swapchain_create_info.height, 321 | ) 322 | else: 323 | GL.glRenderbufferStorageMultisample( 324 | GL.GL_RENDERBUFFER, 325 | self.swapchain_create_info.sample_count, 326 | GL.GL_DEPTH24_STENCIL8, 327 | self.swapchain_create_info.width, 328 | self.swapchain_create_info.height, 329 | ) 330 | self.fbo_id = GL.glGenFramebuffers(1) 331 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, self.fbo_id) 332 | GL.glFramebufferRenderbuffer( 333 | GL.GL_DRAW_FRAMEBUFFER, 334 | GL.GL_DEPTH_STENCIL_ATTACHMENT, 335 | GL.GL_RENDERBUFFER, 336 | self.fbo_depth_buffer, 337 | ) 338 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0) 339 | 340 | def frame(self): 341 | glfw.poll_events() 342 | self.poll_xr_events() 343 | if self.quit: 344 | return 345 | if self.start_xr_frame(): 346 | self.update_xr_views() 347 | if self.frame_state.should_render: 348 | self.render() 349 | self.end_xr_frame() 350 | 351 | def poll_xr_events(self): 352 | while True: 353 | try: 354 | event_buffer = xr.poll_event(self.instance) 355 | event_type = xr.StructureType(event_buffer.type) 356 | if event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED: 357 | self.on_session_state_changed(event_buffer) 358 | except xr.EventUnavailable: 359 | break 360 | 361 | def on_session_state_changed(self, session_state_changed_event): 362 | # TODO: it would be nice to avoid this horrible cast... 363 | event = ctypes.cast( 364 | ctypes.byref(session_state_changed_event), 365 | ctypes.POINTER(xr.EventDataSessionStateChanged)).contents 366 | # TODO: enum property 367 | self.session_state = xr.SessionState(event.state) 368 | if self.session_state == xr.SessionState.READY: 369 | if not self.quit: 370 | sbi = xr.SessionBeginInfo(xr.ViewConfigurationType.PRIMARY_STEREO) 371 | xr.begin_session(self.session, sbi) 372 | elif self.session_state == xr.SessionState.STOPPING: 373 | xr.end_session(self.session) 374 | self.session = None 375 | self.quit = True 376 | 377 | def start_xr_frame(self) -> bool: 378 | if self.session_state in [ 379 | xr.SessionState.READY, 380 | xr.SessionState.FOCUSED, 381 | xr.SessionState.SYNCHRONIZED, 382 | xr.SessionState.VISIBLE, 383 | ]: 384 | frame_wait_info = xr.FrameWaitInfo(None) 385 | try: 386 | self.frame_state = xr.wait_frame(self.session, frame_wait_info) 387 | xr.begin_frame(self.session, None) 388 | return True 389 | except xr.ResultException: 390 | return False 391 | return False 392 | 393 | def end_xr_frame(self): 394 | frame_end_info = xr.FrameEndInfo( 395 | self.frame_state.predicted_display_time, 396 | xr.EnvironmentBlendMode.OPAQUE 397 | ) 398 | if self.frame_state.should_render: 399 | for eye_index in range(2): 400 | layer_view = self.projection_layer_views[eye_index] 401 | eye_view = self.eye_view_states[eye_index] 402 | layer_view.fov = eye_view.fov 403 | layer_view.pose = eye_view.pose 404 | frame_end_info.layers = [ctypes.byref(self.projection_layer), ] 405 | xr.end_frame(self.session, frame_end_info) 406 | 407 | def update_xr_views(self): 408 | vi = xr.ViewLocateInfo( 409 | xr.ViewConfigurationType.PRIMARY_STEREO, 410 | self.frame_state.predicted_display_time, 411 | self.projection_layer.space, 412 | ) 413 | vs, self.eye_view_states = xr.locate_views(self.session, vi) 414 | for eye_index, view_state in enumerate(self.eye_view_states): 415 | # These aren't actually used in this simple example... 416 | # self.eye_projections[eye_index] = something(view_state.fov) # TODO: 417 | # print(view_state.pose) 418 | pass 419 | 420 | def render(self): 421 | ai = xr.SwapchainImageAcquireInfo(None) 422 | swapchain_index = xr.acquire_swapchain_image(self.swapchain, ai) 423 | wi = xr.SwapchainImageWaitInfo(xr.INFINITE_DURATION) 424 | xr.wait_swapchain_image(self.swapchain, wi) 425 | glfw.make_context_current(self.window) 426 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self.fbo_id) 427 | sw_image = self.swapchain_images[swapchain_index] 428 | GL.glFramebufferTexture( 429 | GL.GL_FRAMEBUFFER, 430 | GL.GL_COLOR_ATTACHMENT0, 431 | sw_image.image, 432 | 0, 433 | ) 434 | # "render" to the swapchain image 435 | w, h = self.render_target_size 436 | GL.glEnable(GL.GL_SCISSOR_TEST) 437 | GL.glScissor(0, 0, w // 2, h) 438 | GL.glClearColor(0, 1, 0, 1) 439 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 440 | GL.glScissor(w // 2, 0, w // 2, h) 441 | GL.glClearColor(0, 0, 1, 1) 442 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 443 | GL.glDisable(GL.GL_SCISSOR_TEST) 444 | 445 | if self.mirror_window: 446 | # fast blit from the fbo to the window surface 447 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0) 448 | GL.glBlitFramebuffer( 449 | 0, 0, w, h, 0, 0, 450 | *self.window_size, 451 | GL.GL_COLOR_BUFFER_BIT, 452 | GL.GL_NEAREST 453 | ) 454 | GL.glFramebufferTexture(GL.GL_READ_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, 0, 0) 455 | GL.glBindFramebuffer(GL.GL_READ_FRAMEBUFFER, 0) 456 | ri = xr.SwapchainImageReleaseInfo() 457 | xr.release_swapchain_image(self.swapchain, ri) 458 | # If we're mirror make sure to do the potentially blocking command 459 | # AFTER we've released the swapchain image 460 | if self.mirror_window: 461 | glfw.swap_buffers(self.window) 462 | 463 | def __exit__(self, exc_type, exc_value, traceback): 464 | if self.window is not None: 465 | glfw.make_context_current(self.window) 466 | if self.fbo_id is not None: 467 | GL.glDeleteFramebuffers(1, [self.fbo_id]) 468 | self.fbo_id = None 469 | if self.fbo_depth_buffer is not None: 470 | GL.glDeleteRenderbuffers(1, [self.fbo_depth_buffer]) 471 | self.fbo_depth_buffer = None 472 | glfw.terminate() 473 | self.window = None 474 | if self.swapchain is not None: 475 | xr.destroy_swapchain(self.swapchain) 476 | self.swapchain = None 477 | if self.session is not None: 478 | xr.destroy_session(self.session) 479 | self.session = None 480 | self.system_id = None 481 | if self.instance is not None: 482 | # Workaround for https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422 483 | # and https://github.com/ValveSoftware/SteamVR-for-Linux/issues/479 484 | if not self.linux_steamvr_broken_destroy_instance: 485 | xr.destroy_instance(self.instance) 486 | self.instance = None 487 | glfw.terminate() 488 | 489 | 490 | if __name__ == "__main__": 491 | with OpenXrExample(logging.DEBUG) as ex: 492 | ex.run() 493 | -------------------------------------------------------------------------------- /xr_examples/green_blue.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program green_blue.py 3 | 4 | Different way to get similar display to the venerable gl_example.py 5 | """ 6 | 7 | import os 8 | from OpenGL import GL 9 | import xr 10 | 11 | 12 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 13 | with xr.ContextObject( 14 | instance_create_info=xr.InstanceCreateInfo( 15 | enabled_extension_names=[ 16 | # A graphics extension is mandatory (without a headless extension) 17 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 18 | ], 19 | ), 20 | ) as context: 21 | eye_colors = [ 22 | (0, 1, 0, 1), # Left eye green 23 | (0, 0, 1, 1), # Right eye blue 24 | (1, 0, 0, 1), # Third eye blind 25 | ] 26 | for frame_index, frame_state in enumerate(context.frame_loop()): 27 | for view_index, view in enumerate(context.view_loop(frame_state)): 28 | # set each eye to a different color (not working yet...) 29 | GL.glClearColor(*eye_colors[view_index]) 30 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 31 | if frame_index > 500: # Don't run forever 32 | break 33 | -------------------------------------------------------------------------------- /xr_examples/headless.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr headless example 3 | using low level OpenXR API 4 | """ 5 | 6 | import ctypes 7 | import platform 8 | import time 9 | import xr 10 | 11 | # Enumerate the required instance extensions 12 | extensions = [xr.MND_HEADLESS_EXTENSION_NAME] # Permits use without a graphics display 13 | # Tracking controllers in headless mode requires a way to get the current XrTime 14 | if platform.system() == "Windows": 15 | extensions.append(xr.KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME) 16 | else: # Linux 17 | extensions.append(xr.KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME) 18 | 19 | # Create instance for headless use 20 | instance = xr.create_instance(xr.InstanceCreateInfo( 21 | enabled_extension_names=extensions, 22 | )) 23 | system = xr.get_system( 24 | instance, 25 | # Presumably the form factor is irrelevant in headless mode... 26 | xr.SystemGetInfo(form_factor=xr.FormFactor.HEAD_MOUNTED_DISPLAY), 27 | ) 28 | session = xr.create_session( 29 | instance, 30 | xr.SessionCreateInfo( 31 | system_id=system, 32 | next=None, # No GraphicsBinding structure is required here in HEADLESS mode 33 | ) 34 | ) 35 | 36 | if platform.system() == "Windows": 37 | import ctypes.wintypes 38 | pc_time = ctypes.wintypes.LARGE_INTEGER() 39 | kernel32 = ctypes.WinDLL("kernel32") 40 | pxrConvertWin32PerformanceCounterToTimeKHR = ctypes.cast( 41 | xr.get_instance_proc_addr( 42 | instance=instance, 43 | name="xrConvertWin32PerformanceCounterToTimeKHR", 44 | ), 45 | xr.PFN_xrConvertWin32PerformanceCounterToTimeKHR, 46 | ) 47 | 48 | def time_from_perf_counter(instance: xr.Instance, 49 | performance_counter: ctypes.wintypes.LARGE_INTEGER) -> xr.Time: 50 | xr_time = xr.Time() 51 | result = pxrConvertWin32PerformanceCounterToTimeKHR( 52 | instance, 53 | ctypes.pointer(performance_counter), 54 | ctypes.byref(xr_time), 55 | ) 56 | result = xr.check_result(result) 57 | if result.is_exception(): 58 | raise result 59 | return xr_time 60 | else: 61 | timespecTime = xr.timespec() 62 | pxrConvertTimespecTimeToTimeKHR = ctypes.cast( 63 | xr.get_instance_proc_addr( 64 | instance=instance, 65 | name="xrConvertTimespecTimeToTimeKHR", 66 | ), 67 | xr.PFN_xrConvertTimespecTimeToTimeKHR, 68 | ) 69 | 70 | def time_from_timespec(instance: xr.Instance, timespec_time: xr.timespec) -> xr.Time: 71 | xr_time = xr.Time() 72 | result = pxrConvertTimespecTimeToTimeKHR( 73 | instance, 74 | ctypes.pointer(timespec_time), 75 | ctypes.byref(xr_time), 76 | ) 77 | result = xr.check_result(result) 78 | if result.is_exception(): 79 | raise result 80 | return xr_time 81 | # Set up controller tracking, as one possible legitimate headless activity 82 | action_set = xr.create_action_set( 83 | instance=instance, 84 | create_info=xr.ActionSetCreateInfo( 85 | action_set_name="action_set", 86 | localized_action_set_name="Action Set", 87 | priority=0, 88 | ), 89 | ) 90 | controller_paths = (xr.Path * 2)( 91 | xr.string_to_path(instance, "/user/hand/left"), 92 | xr.string_to_path(instance, "/user/hand/right"), 93 | ) 94 | controller_pose_action = xr.create_action( 95 | action_set=action_set, 96 | create_info=xr.ActionCreateInfo( 97 | action_type=xr.ActionType.POSE_INPUT, 98 | action_name="controller_pose", 99 | localized_action_name="Controller pose", 100 | count_subaction_paths=len(controller_paths), 101 | subaction_paths=controller_paths, 102 | ), 103 | ) 104 | suggested_bindings = (xr.ActionSuggestedBinding * 2)( 105 | xr.ActionSuggestedBinding( 106 | action=controller_pose_action, 107 | binding=xr.string_to_path( 108 | instance=instance, 109 | path_string="/user/hand/left/input/grip/pose", 110 | ), 111 | ), 112 | xr.ActionSuggestedBinding( 113 | action=controller_pose_action, 114 | binding=xr.string_to_path( 115 | instance=instance, 116 | path_string="/user/hand/right/input/grip/pose", 117 | ), 118 | ), 119 | ) 120 | xr.suggest_interaction_profile_bindings( 121 | instance=instance, 122 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 123 | interaction_profile=xr.string_to_path( 124 | instance, 125 | "/interaction_profiles/khr/simple_controller", 126 | ), 127 | count_suggested_bindings=len(suggested_bindings), 128 | suggested_bindings=suggested_bindings, 129 | ), 130 | ) 131 | xr.suggest_interaction_profile_bindings( 132 | instance=instance, 133 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 134 | interaction_profile=xr.string_to_path( 135 | instance, 136 | "/interaction_profiles/htc/vive_controller", 137 | ), 138 | count_suggested_bindings=len(suggested_bindings), 139 | suggested_bindings=suggested_bindings, 140 | ), 141 | ) 142 | xr.attach_session_action_sets( 143 | session=session, 144 | attach_info=xr.SessionActionSetsAttachInfo( 145 | action_sets=[action_set], 146 | ), 147 | ) 148 | action_spaces = [ 149 | xr.create_action_space( 150 | session=session, 151 | create_info=xr.ActionSpaceCreateInfo( 152 | action=controller_pose_action, 153 | subaction_path=controller_paths[0], 154 | ), 155 | ), 156 | xr.create_action_space( 157 | session=session, 158 | create_info=xr.ActionSpaceCreateInfo( 159 | action=controller_pose_action, 160 | subaction_path=controller_paths[1], 161 | ), 162 | ), 163 | ] 164 | reference_space = xr.create_reference_space( 165 | session=session, 166 | create_info=xr.ReferenceSpaceCreateInfo( 167 | reference_space_type=xr.ReferenceSpaceType.STAGE, 168 | ), 169 | ) 170 | view_reference_space = xr.create_reference_space( 171 | session=session, 172 | create_info=xr.ReferenceSpaceCreateInfo( 173 | reference_space_type=xr.ReferenceSpaceType.VIEW, 174 | ), 175 | ) 176 | session_state = xr.SessionState.UNKNOWN 177 | # Loop over session frames 178 | for frame_index in range(30): # Limit number of frames for demo purposes 179 | # Poll session state changed events 180 | while True: 181 | try: 182 | event_buffer = xr.poll_event(instance) 183 | event_type = xr.StructureType(event_buffer.type) 184 | if event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED: 185 | event = ctypes.cast( 186 | ctypes.byref(event_buffer), 187 | ctypes.POINTER(xr.EventDataSessionStateChanged)).contents 188 | session_state = xr.SessionState(event.state) 189 | print(f"OpenXR session state changed to xr.SessionState.{session_state.name}") 190 | if session_state == xr.SessionState.READY: 191 | xr.begin_session( 192 | session, 193 | xr.SessionBeginInfo( 194 | # TODO: zero should be allowed here... 195 | primary_view_configuration_type=xr.ViewConfigurationType.PRIMARY_MONO, 196 | ), 197 | ) 198 | elif session_state == xr.SessionState.STOPPING: 199 | xr.destroy_session(session) 200 | session = None 201 | except xr.EventUnavailable: 202 | break # There is no event in the queue at this moment 203 | if session_state == xr.SessionState.FOCUSED: 204 | # wait_frame()/begin_frame()/end_frame() are not required in headless mode 205 | xr.wait_frame(session=session) # Helps SteamVR show application name better 206 | # Perform per-frame activities here 207 | 208 | if platform.system() == "Windows": 209 | kernel32.QueryPerformanceCounter(ctypes.byref(pc_time)) 210 | xr_time_now = time_from_perf_counter(instance, pc_time) 211 | else: 212 | time_float = time.clock_gettime(time.CLOCK_MONOTONIC) 213 | timespecTime.tv_sec = int(time_float) 214 | timespecTime.tv_nsec = int((time_float % 1) * 1e9) 215 | xr_time_now = time_from_timespec(instance, timespecTime) 216 | 217 | active_action_set = xr.ActiveActionSet( 218 | action_set=action_set, 219 | subaction_path=xr.NULL_PATH, 220 | ) 221 | xr.sync_actions( 222 | session=session, 223 | sync_info=xr.ActionsSyncInfo( 224 | active_action_sets=[active_action_set], 225 | ), 226 | ) 227 | found_count = 0 228 | hmd_location = xr.locate_space( 229 | space=view_reference_space, 230 | base_space=reference_space, 231 | time=xr_time_now, 232 | ) 233 | if hmd_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: 234 | print(f"HMD location: {hmd_location.pose}") 235 | found_count += 1 236 | for index, space in enumerate(action_spaces): 237 | space_location = xr.locate_space( 238 | space=space, 239 | base_space=reference_space, 240 | time=xr_time_now, 241 | ) 242 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: 243 | print(f"Controller {index + 1}: {space_location.pose}") 244 | found_count += 1 245 | if found_count == 0: 246 | print("no controllers active") 247 | 248 | # Sleep periodically to avoid consuming all available system resources 249 | time.sleep(0.500) 250 | 251 | # Clean up 252 | system = xr.NULL_SYSTEM_ID 253 | xr.destroy_action_set(action_set) 254 | action_set = None 255 | xr.destroy_instance(instance) 256 | instance = None 257 | -------------------------------------------------------------------------------- /xr_examples/hello_xr.py: -------------------------------------------------------------------------------- 1 | from xr_examples.hello_xr.main import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on example from https://github.com/KhronosGroup/OpenXR-SDK-Source/tree/main/src/tests/hello_xr 3 | """ 4 | 5 | from xr_examples.hello_xr.main import main 6 | 7 | if __name__ == "__main__": 8 | main() 9 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/geometry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2017-2022, The Khronos Group Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | import ctypes 7 | import numpy 8 | import xr 9 | 10 | 11 | class Vertex(ctypes.Structure): 12 | _fields_ = [ 13 | ("position", xr.Vector3f), 14 | ("color", xr.Vector3f), 15 | ] 16 | 17 | 18 | Red = numpy.array([1, 0, 0], dtype=numpy.float32) 19 | DarkRed = numpy.array([0.25, 0, 0], dtype=numpy.float32) 20 | Green = numpy.array([0, 1, 0], dtype=numpy.float32) 21 | DarkGreen = numpy.array([0, 0.25, 0], dtype=numpy.float32) 22 | Blue = numpy.array([0, 0, 1], dtype=numpy.float32) 23 | DarkBlue = numpy.array([0, 0, 0.25], dtype=numpy.float32) 24 | 25 | # Vertices for a 1x1x1 meter cube. (Left/Right, Top/Bottom, Front/Back) 26 | LBB = numpy.array([-0.5, -0.5, -0.5], dtype=numpy.float32) 27 | LBF = numpy.array([-0.5, -0.5, 0.5], dtype=numpy.float32) 28 | LTB = numpy.array([-0.5, 0.5, -0.5], dtype=numpy.float32) 29 | LTF = numpy.array([-0.5, 0.5, 0.5], dtype=numpy.float32) 30 | RBB = numpy.array([0.5, -0.5, -0.5], dtype=numpy.float32) 31 | RBF = numpy.array([0.5, -0.5, 0.5], dtype=numpy.float32) 32 | RTB = numpy.array([0.5, 0.5, -0.5], dtype=numpy.float32) 33 | RTF = numpy.array([0.5, 0.5, 0.5], dtype=numpy.float32) 34 | 35 | 36 | def cube_side(v1, v2, v3, v4, v5, v6, color): 37 | return numpy.array([ 38 | [v1, color], [v2, color], [v3, color], [v4, color], [v5, color], [v6, color], 39 | ], dtype=numpy.float32) 40 | 41 | 42 | c_cubeVertices = numpy.array([ 43 | cube_side(LTB, LBF, LBB, LTB, LTF, LBF, DarkRed), # -X 44 | cube_side(RTB, RBB, RBF, RTB, RBF, RTF, Red), # +X 45 | cube_side(LBB, LBF, RBF, LBB, RBF, RBB, DarkGreen), # -Y 46 | cube_side(LTB, RTB, RTF, LTB, RTF, LTF, Green), # +Y 47 | cube_side(LBB, RBB, RTB, LBB, RTB, LTB, DarkBlue), # -Z 48 | cube_side(LBF, LTF, RTF, LBF, RTF, RBF, Blue), # +Z 49 | ], dtype=numpy.float32) 50 | 51 | # Winding order is clockwise. Each side uses a different color. 52 | c_cubeIndices = numpy.array([ 53 | 0, 1, 2, 3, 4, 5, # -X 54 | 6, 7, 8, 9, 10, 11, # +X 55 | 12, 13, 14, 15, 16, 17, # -Y 56 | 18, 19, 20, 21, 22, 23, # +Y 57 | 24, 25, 26, 27, 28, 29, # -Z 58 | 30, 31, 32, 33, 34, 35, # +Z 59 | ], dtype=numpy.uint16) 60 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/graphics_plugin.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import ctypes 3 | from typing import List, Union 4 | 5 | import xr 6 | 7 | 8 | class Cube(ctypes.Structure): 9 | _fields_ = [ 10 | ("Pose", xr.Posef), 11 | ("Scale", xr.Vector3f), 12 | ] 13 | 14 | 15 | class IGraphicsPlugin(abc.ABC): 16 | """Wraps a graphics API so the main openxr program can be graphics API-independent.""" 17 | 18 | @abc.abstractmethod 19 | def __enter__(self): 20 | pass 21 | 22 | @abc.abstractmethod 23 | def __exit__(self, exc_type, exc_val, exc_tb): 24 | """Clean up graphics resources.""" 25 | 26 | """ 27 | AllocateSwapchainImageStructs is not implemented in python. 28 | Because unlike C++, we can use the swapchain_image_type property. 29 | """ 30 | 31 | @abc.abstractmethod 32 | def get_supported_swapchain_sample_count(self, xr_view_configuration_view: xr.ViewConfigurationView) -> int: 33 | """ 34 | Get recommended number of sub-data element samples in view (recommendedSwapchainSampleCount) 35 | if supported by the graphics plugin. A supported value otherwise. 36 | """ 37 | 38 | @property 39 | @abc.abstractmethod 40 | def graphics_binding(self) -> ctypes.Structure: 41 | """Get the graphics binding header for session creation.""" 42 | 43 | @abc.abstractmethod 44 | def initialize_device(self, instance: xr.Instance, system_id: xr.SystemId) -> None: 45 | """Create an instance of this graphics api for the provided instance and systemId.""" 46 | 47 | @property 48 | @abc.abstractmethod 49 | def instance_extensions(self) -> List[str]: 50 | """OpenXR extensions required by this graphics API.""" 51 | 52 | @abc.abstractmethod 53 | def poll_events(self) -> bool: 54 | """""" 55 | 56 | @abc.abstractmethod 57 | def render_view(self, layer_view: xr.CompositionLayerProjectionView, swapchain_image: xr.SwapchainImageBaseHeader, 58 | swapchain_format: int, cubes: List[Cube], mirror=False): 59 | """Render to a swapchain image for a projection view.""" 60 | 61 | @abc.abstractmethod 62 | def select_color_swapchain_format(self, runtime_formats: Union[List[int], ctypes.Array]) -> int: 63 | """Select the preferred swapchain format from the list of available formats.""" 64 | 65 | @property 66 | @abc.abstractmethod 67 | def swapchain_image_type(self): 68 | """The type of xr swapchain image used by this graphics plugin.""" 69 | 70 | @abc.abstractmethod 71 | def update_options(self, options) -> None: 72 | pass 73 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/graphics_plugin_opengl.py: -------------------------------------------------------------------------------- 1 | from ctypes import byref, c_void_p, cast, sizeof, POINTER, Structure 2 | import inspect 3 | import logging 4 | import platform 5 | from typing import Dict, List, Optional 6 | 7 | import numpy 8 | from OpenGL import GL 9 | 10 | from .graphics_plugin import Cube, IGraphicsPlugin 11 | 12 | if platform.system() == "Windows": 13 | from OpenGL import WGL 14 | elif platform.system() == "Linux": 15 | from OpenGL import GLX 16 | import glfw 17 | 18 | import xr 19 | 20 | from .geometry import c_cubeVertices, c_cubeIndices, Vertex 21 | from .linear import GraphicsAPI, Matrix4x4f 22 | from .options import Options 23 | 24 | logger = logging.getLogger("hello_xr.graphics_plugin_opengl") 25 | 26 | 27 | dark_slate_gray = numpy.array([0.184313729, 0.309803933, 0.309803933, 1.0], dtype=numpy.float32) 28 | 29 | vertex_shader_glsl = inspect.cleandoc(""" 30 | #version 410 31 | 32 | in vec3 VertexPos; 33 | in vec3 VertexColor; 34 | 35 | out vec3 PSVertexColor; 36 | 37 | uniform mat4 ModelViewProjection; 38 | 39 | void main() { 40 | gl_Position = ModelViewProjection * vec4(VertexPos, 1.0); 41 | PSVertexColor = VertexColor; 42 | } 43 | """) 44 | 45 | fragment_shader_glsl = inspect.cleandoc(""" 46 | #version 410 47 | 48 | in vec3 PSVertexColor; 49 | out vec4 FragColor; 50 | 51 | void main() { 52 | FragColor = vec4(PSVertexColor, 1); 53 | } 54 | """) 55 | 56 | 57 | class OpenGLGraphicsPlugin(IGraphicsPlugin): 58 | def __init__(self, options: Options): 59 | super().__init__() 60 | self.window = None 61 | 62 | self.background_clear_color = options.background_clear_color 63 | if platform.system() == "Windows": 64 | self._graphics_binding = xr.GraphicsBindingOpenGLWin32KHR() 65 | elif platform.system() == "Linux": 66 | # TODO more nuance on Linux: Xlib, Xcb, Wayland 67 | self._graphics_binding = xr.GraphicsBindingOpenGLXlibKHR() 68 | self.swapchain_image_buffers: List[xr.SwapchainImageOpenGLKHR] = [] # To keep the swapchain images alive 69 | self.swapchain_framebuffer: Optional[int] = None 70 | self.program = None 71 | self.model_view_projection_uniform_location = 0 72 | self.vertex_attrib_coords = 0 73 | self.vertex_attrib_color = 0 74 | self.vao = None 75 | self.cube_vertex_buffer = None 76 | self.cube_index_buffer = None 77 | # Map color buffer to associated depth buffer. This map is populated on demand. 78 | self.color_to_depth_map: Dict[int, int] = {} 79 | self.debug_message_proc = None # To keep the callback alive 80 | 81 | def __enter__(self): 82 | return self 83 | 84 | def __exit__(self, exc_type, exc_val, exc_tb): 85 | if self.window: 86 | glfw.make_context_current(self.window) 87 | if self.swapchain_framebuffer is not None: 88 | GL.glDeleteFramebuffers(1, [self.swapchain_framebuffer]) 89 | if self.program is not None: 90 | GL.glDeleteProgram(self.program) 91 | if self.vao is not None: 92 | GL.glDeleteVertexArrays(1, [self.vao]) 93 | if self.cube_vertex_buffer is not None: 94 | GL.glDeleteBuffers(1, [self.cube_vertex_buffer]) 95 | if self.cube_index_buffer is not None: 96 | GL.glDeleteBuffers(1, [self.cube_index_buffer]) 97 | self.swapchain_framebuffer = None 98 | self.program = None 99 | self.vao = None 100 | self.cube_vertex_buffer = None 101 | self.cube_index_buffer = None 102 | for color, depth in self.color_to_depth_map.items(): 103 | if depth is not None: 104 | GL.glDeleteTextures(1, [depth]) 105 | self.color_to_depth_map = {} 106 | if self.window is not None: 107 | glfw.destroy_window(self.window) 108 | self.window = None 109 | glfw.terminate() 110 | 111 | @staticmethod 112 | def check_shader(shader): 113 | result = GL.glGetShaderiv(shader, GL.GL_COMPILE_STATUS) 114 | if not result: 115 | raise RuntimeError(f"Compile shader failed: {GL.glGetShaderInfoLog(shader)}") 116 | 117 | @staticmethod 118 | def check_program(prog): 119 | result = GL.glGetProgramiv(prog, GL.GL_LINK_STATUS) 120 | if not result: 121 | raise RuntimeError(f"Link program failed: {GL.glGetProgramInfoLog(prog)}") 122 | 123 | @staticmethod 124 | def opengl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user): 125 | """Redirect OpenGL debug messages""" 126 | log_level = { 127 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR, 128 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING, 129 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO, 130 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG, 131 | }[severity] 132 | logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}") 133 | 134 | def focus_window(self): 135 | glfw.focus_window(self.window) 136 | glfw.make_context_current(self.window) 137 | 138 | def get_depth_texture(self, color_texture) -> int: 139 | # If a depth-stencil view has already been created for this back-buffer, use it. 140 | if color_texture in self.color_to_depth_map: 141 | return self.color_to_depth_map[color_texture] 142 | # This back-buffer has no corresponding depth-stencil texture, so create one with matching dimensions. 143 | GL.glBindTexture(GL.GL_TEXTURE_2D, color_texture) 144 | width = GL.glGetTexLevelParameteriv(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_WIDTH) 145 | height = GL.glGetTexLevelParameteriv(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_HEIGHT) 146 | 147 | depth_texture = GL.glGenTextures(1) 148 | GL.glBindTexture(GL.GL_TEXTURE_2D, depth_texture) 149 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST) 150 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST) 151 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE) 152 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE) 153 | GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_DEPTH_COMPONENT32, width, height, 0, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, None) 154 | self.color_to_depth_map[color_texture] = depth_texture 155 | return depth_texture 156 | 157 | def get_supported_swapchain_sample_count(self, _xr_view_configuration_view: xr.ViewConfigurationView): 158 | return 1 159 | 160 | @property 161 | def instance_extensions(self) -> List[str]: 162 | return [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME] 163 | 164 | @property 165 | def swapchain_image_type(self): 166 | return xr.SwapchainImageOpenGLKHR 167 | 168 | @property 169 | def graphics_binding(self) -> Structure: 170 | return self._graphics_binding 171 | 172 | def initialize_device(self, instance: xr.Instance, system_id: xr.SystemId): 173 | # extension function must be loaded by name 174 | pfn_get_open_gl_graphics_requirements_khr = cast( 175 | xr.get_instance_proc_addr( 176 | instance, 177 | "xrGetOpenGLGraphicsRequirementsKHR", 178 | ), 179 | xr.PFN_xrGetOpenGLGraphicsRequirementsKHR 180 | ) 181 | graphics_requirements = xr.GraphicsRequirementsOpenGLKHR() 182 | result = pfn_get_open_gl_graphics_requirements_khr(instance, system_id, byref(graphics_requirements)) 183 | result = xr.check_result(xr.Result(result)) 184 | if result.is_exception(): 185 | raise result 186 | # Initialize the gl extensions. Note we have to open a window. 187 | if not glfw.init(): 188 | raise xr.XrException("GLFW initialization failed") 189 | glfw.window_hint(glfw.DOUBLEBUFFER, False) 190 | glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4) 191 | glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 5) 192 | glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) 193 | self.window = glfw.create_window(640, 480, "GLFW Window", None, None) 194 | if self.window is None: 195 | raise xr.XrException("Failed to create GLFW window") 196 | glfw.make_context_current(self.window) 197 | glfw.show_window(self.window) 198 | glfw.swap_interval(0) 199 | glfw.focus_window(self.window) 200 | major = GL.glGetIntegerv(GL.GL_MAJOR_VERSION) 201 | minor = GL.glGetIntegerv(GL.GL_MINOR_VERSION) 202 | logger.debug(f"OpenGL version {major}.{minor}") 203 | desired_api_version = xr.Version(major, minor, 0) 204 | if graphics_requirements.min_api_version_supported > desired_api_version.number(): 205 | ms = xr.Version(graphics_requirements.min_api_version_supported).number() 206 | raise xr.XrException(f"Runtime does not support desired Graphics API and/or version {hex(ms)}") 207 | if platform.system() == "Windows": 208 | self._graphics_binding.h_dc = WGL.wglGetCurrentDC() 209 | self._graphics_binding.h_glrc = WGL.wglGetCurrentContext() 210 | elif platform.system() == "Linux": 211 | # TODO more nuance on Linux: Xlib, Xcb, Wayland 212 | self._graphics_binding.x_display = GLX.glXGetCurrentDisplay() 213 | self._graphics_binding.glx_drawable = GLX.glXGetCurrentDrawable() 214 | self._graphics_binding.glx_context = GLX.glXGetCurrentContext() 215 | GL.glEnable(GL.GL_DEBUG_OUTPUT) 216 | # Store the debug callback function pointer, so it won't get garbage collected; 217 | # ...otherwise mysterious GL crashes will ensue. 218 | self.debug_message_proc = GL.GLDEBUGPROC(self.opengl_debug_message_callback) 219 | GL.glDebugMessageCallback(self.debug_message_proc, None) 220 | self.initialize_resources() 221 | 222 | def initialize_resources(self): 223 | self.swapchain_framebuffer = GL.glGenFramebuffers(1) 224 | vertex_shader = GL.glCreateShader(GL.GL_VERTEX_SHADER) 225 | GL.glShaderSource(vertex_shader, vertex_shader_glsl) 226 | GL.glCompileShader(vertex_shader) 227 | self.check_shader(vertex_shader) 228 | fragment_shader = GL.glCreateShader(GL.GL_FRAGMENT_SHADER) 229 | GL.glShaderSource(fragment_shader, fragment_shader_glsl) 230 | GL.glCompileShader(fragment_shader) 231 | self.check_shader(fragment_shader) 232 | self.program = GL.glCreateProgram() 233 | GL.glAttachShader(self.program, vertex_shader) 234 | GL.glAttachShader(self.program, fragment_shader) 235 | GL.glLinkProgram(self.program) 236 | self.check_program(self.program) 237 | GL.glDeleteShader(vertex_shader) 238 | GL.glDeleteShader(fragment_shader) 239 | self.model_view_projection_uniform_location = GL.glGetUniformLocation(self.program, "ModelViewProjection") 240 | self.vertex_attrib_coords = GL.glGetAttribLocation(self.program, "VertexPos") 241 | self.vertex_attrib_color = GL.glGetAttribLocation(self.program, "VertexColor") 242 | self.cube_vertex_buffer = GL.glGenBuffers(1) 243 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.cube_vertex_buffer) 244 | GL.glBufferData(GL.GL_ARRAY_BUFFER, c_cubeVertices, GL.GL_STATIC_DRAW) 245 | self.cube_index_buffer = GL.glGenBuffers(1) 246 | GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.cube_index_buffer) 247 | GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, c_cubeIndices, GL.GL_STATIC_DRAW) 248 | self.vao = GL.glGenVertexArrays(1) 249 | GL.glBindVertexArray(self.vao) 250 | GL.glEnableVertexAttribArray(self.vertex_attrib_coords) 251 | GL.glEnableVertexAttribArray(self.vertex_attrib_color) 252 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.cube_vertex_buffer) 253 | GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.cube_index_buffer) 254 | GL.glVertexAttribPointer(self.vertex_attrib_coords, 3, GL.GL_FLOAT, False, 255 | sizeof(Vertex), cast(0, c_void_p)) 256 | GL.glVertexAttribPointer(self.vertex_attrib_color, 3, GL.GL_FLOAT, False, 257 | sizeof(Vertex), 258 | cast(sizeof(xr.Vector3f), c_void_p)) 259 | 260 | def poll_events(self) -> bool: 261 | glfw.poll_events() 262 | return glfw.window_should_close(self.window) 263 | 264 | def render_view( 265 | self, 266 | layer_view: xr.CompositionLayerProjectionView, 267 | swapchain_image_base_ptr: POINTER(xr.SwapchainImageBaseHeader), 268 | _swapchain_format: int, 269 | cubes: List[Cube], 270 | mirror=False, 271 | ): 272 | assert layer_view.sub_image.image_array_index == 0 # texture arrays not supported. 273 | # UNUSED_PARM(swapchain_format) # not used in this function for now. 274 | glfw.make_context_current(self.window) 275 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self.swapchain_framebuffer) 276 | swapchain_image = cast(swapchain_image_base_ptr, POINTER(xr.SwapchainImageOpenGLKHR)).contents 277 | color_texture = swapchain_image.image 278 | GL.glViewport(layer_view.sub_image.image_rect.offset.x, 279 | layer_view.sub_image.image_rect.offset.y, 280 | layer_view.sub_image.image_rect.extent.width, 281 | layer_view.sub_image.image_rect.extent.height) 282 | GL.glFrontFace(GL.GL_CW) 283 | GL.glCullFace(GL.GL_BACK) 284 | GL.glEnable(GL.GL_CULL_FACE) 285 | GL.glEnable(GL.GL_DEPTH_TEST) 286 | depth_texture = self.get_depth_texture(color_texture) 287 | GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D, color_texture, 0) 288 | GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT, GL.GL_TEXTURE_2D, depth_texture, 0) 289 | # Clear swapchain and depth buffer. 290 | GL.glClearColor(*self.background_clear_color) 291 | GL.glClearDepth(1.0) 292 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT) 293 | # Set shaders and uniform variables. 294 | GL.glUseProgram(self.program) 295 | pose = layer_view.pose 296 | proj = Matrix4x4f.create_projection_fov(GraphicsAPI.OPENGL, layer_view.fov, 0.05, 100.0) 297 | scale = xr.Vector3f(1, 1, 1) 298 | to_view = Matrix4x4f.create_translation_rotation_scale(pose.position, pose.orientation, scale) 299 | view = Matrix4x4f.invert_rigid_body(to_view) 300 | vp = proj @ view 301 | # Set cube primitive data. 302 | GL.glBindVertexArray(self.vao) 303 | # Render each cube 304 | for cube in cubes: 305 | # Compute the model-view-projection transform and set it. 306 | model = Matrix4x4f.create_translation_rotation_scale(cube.Pose.position, cube.Pose.orientation, cube.Scale) 307 | mvp = vp @ model 308 | GL.glUniformMatrix4fv(self.model_view_projection_uniform_location, 1, False, mvp.as_numpy()) 309 | # Draw the cube. 310 | GL.glDrawElements(GL.GL_TRIANGLES, len(c_cubeIndices), GL.GL_UNSIGNED_SHORT, None) 311 | 312 | if mirror: 313 | # fast blit from the fbo to the window surface 314 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0) 315 | w, h = layer_view.sub_image.image_rect.extent.width, layer_view.sub_image.image_rect.extent.height 316 | GL.glBlitFramebuffer( 317 | 0, 0, w, h, 0, 0, 318 | 640, 480, 319 | GL.GL_COLOR_BUFFER_BIT, 320 | GL.GL_NEAREST 321 | ) 322 | 323 | GL.glBindVertexArray(0) 324 | GL.glUseProgram(0) 325 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) 326 | 327 | def select_color_swapchain_format(self, runtime_formats): 328 | # List of supported color swapchain formats. 329 | supported_color_swapchain_formats = [ 330 | GL.GL_RGB10_A2, 331 | GL.GL_RGBA16F, 332 | # The two below should only be used as a fallback, as they are linear color formats without enough bits for color 333 | # depth, thus leading to banding. 334 | GL.GL_RGBA8, 335 | GL.GL_RGBA8_SNORM, 336 | # These two below are the only color formats reported by Steam VR beta 1.24.2 337 | GL.GL_SRGB8, 338 | GL.GL_SRGB8_ALPHA8, 339 | ] 340 | for rf in runtime_formats: 341 | for sf in supported_color_swapchain_formats: 342 | if rf == sf: 343 | return sf 344 | raise RuntimeError("No runtime swapchain format supported for color swapchain") 345 | 346 | def update_options(self, options) -> None: 347 | self.background_clear_color = options.background_clear_color 348 | 349 | def window_should_close(self): 350 | return glfw.window_should_close(self.window) 351 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/linear.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Copyright (c) 2017 The Khronos Group Inc. 3 | # Copyright (c) 2016 Oculus VR, LLC. 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License") 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http:#www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # Author: J.M.P. van Waveren 20 | # 21 | """ 22 | 23 | from ctypes import addressof, c_float, Structure 24 | import enum 25 | import math 26 | import numpy 27 | 28 | import xr 29 | 30 | 31 | class GraphicsAPI(enum.Enum): 32 | VULKAN = 0, 33 | OPENGL = 1, 34 | OPENGL_ES = 2, 35 | D3D = 3, 36 | 37 | 38 | # Column-major, pre-multiplied. This type does not exist in the OpenXR API and is provided for convenience. 39 | class Matrix4x4f(Structure): 40 | _fields_ = [("m", c_float * 16), ] 41 | 42 | def __init__(self): 43 | super().__init__() 44 | self._numpy = None 45 | 46 | def __matmul__(self, other) -> "Matrix4x4f": 47 | return self.multiply(other) 48 | 49 | def as_numpy(self): 50 | if not hasattr(self, "_numpy") or self._numpy is None: 51 | # Just in time construction 52 | buffer = (c_float * 16).from_address(addressof(self)) 53 | buffer._wrapper = self # To link lifetime of buffer to self 54 | self._numpy = numpy.ctypeslib.as_array(buffer) 55 | return self._numpy 56 | 57 | @staticmethod 58 | def create_from_quaternion(quat: xr.Quaternionf) -> "Matrix4x4f": 59 | """ Creates a matrix from a quaternion. """ 60 | x2 = quat.x + quat.x 61 | y2 = quat.y + quat.y 62 | z2 = quat.z + quat.z 63 | 64 | xx2 = quat.x * x2 65 | yy2 = quat.y * y2 66 | zz2 = quat.z * z2 67 | 68 | yz2 = quat.y * z2 69 | wx2 = quat.w * x2 70 | xy2 = quat.x * y2 71 | wz2 = quat.w * z2 72 | xz2 = quat.x * z2 73 | wy2 = quat.w * y2 74 | 75 | result = Matrix4x4f() 76 | result.m[0] = 1.0 - yy2 - zz2 77 | result.m[1] = xy2 + wz2 78 | result.m[2] = xz2 - wy2 79 | result.m[3] = 0.0 80 | 81 | result.m[4] = xy2 - wz2 82 | result.m[5] = 1.0 - xx2 - zz2 83 | result.m[6] = yz2 + wx2 84 | result.m[7] = 0.0 85 | 86 | result.m[8] = xz2 + wy2 87 | result.m[9] = yz2 - wx2 88 | result.m[10] = 1.0 - xx2 - yy2 89 | result.m[11] = 0.0 90 | 91 | result.m[12] = 0.0 92 | result.m[13] = 0.0 93 | result.m[14] = 0.0 94 | result.m[15] = 1.0 95 | return result 96 | 97 | @staticmethod 98 | def create_projection(graphics_api: GraphicsAPI, tan_angle_left: float, tan_angle_right: float, tan_angle_up: float, 99 | tan_angle_down: float, near_z: float, far_z: float) -> "Matrix4x4f": 100 | """ 101 | # Creates a projection matrix based on the specified dimensions. 102 | # The projection matrix transforms -Z=forward, +Y=up, +X=right to the appropriate clip space for the graphics API. 103 | # The far plane is placed at infinity if far_z <= near_z. 104 | # An infinite projection matrix is preferred for rasterization because, except for 105 | # things *right* up against the near plane, it always provides better precision: 106 | # "Tightening the Precision of Perspective Rendering" 107 | # Paul Upchurch, Mathieu Desbrun 108 | # Journal of Graphics Tools, Volume 16, Issue 1, 2012 109 | """ 110 | tan_angle_width = tan_angle_right - tan_angle_left 111 | # Set to tan_angle_down - tan_angle_up for a clip space with positive Y down (Vulkan). 112 | # Set to tan_angle_up - tan_angle_down for a clip space with positive Y up (OpenGL / D3D / Metal). 113 | tan_angle_height = (tan_angle_down - tan_angle_up) if graphics_api == GraphicsAPI.VULKAN else (tan_angle_up - tan_angle_down) 114 | # Set to near_z for a [-1,1] Z clip space (OpenGL / OpenGL ES). 115 | # Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal). 116 | offset_z = near_z if graphics_api == GraphicsAPI.OPENGL or graphics_api == GraphicsAPI.OPENGL_ES else 0 117 | result = Matrix4x4f() 118 | if far_z <= near_z: 119 | # place the far plane at infinity 120 | result.m[0] = 2.0 / tan_angle_width 121 | result.m[4] = 0.0 122 | result.m[8] = (tan_angle_right + tan_angle_left) / tan_angle_width 123 | result.m[12] = 0.0 124 | 125 | result.m[1] = 0.0 126 | result.m[5] = 2.0 / tan_angle_height 127 | result.m[9] = (tan_angle_up + tan_angle_down) / tan_angle_height 128 | result.m[13] = 0.0 129 | 130 | result.m[2] = 0.0 131 | result.m[6] = 0.0 132 | result.m[10] = -1.0 133 | result.m[14] = -(near_z + offset_z) 134 | 135 | result.m[3] = 0.0 136 | result.m[7] = 0.0 137 | result.m[11] = -1.0 138 | result.m[15] = 0.0 139 | else: 140 | # normal projection 141 | result.m[0] = 2.0 / tan_angle_width 142 | result.m[4] = 0.0 143 | result.m[8] = (tan_angle_right + tan_angle_left) / tan_angle_width 144 | result.m[12] = 0.0 145 | 146 | result.m[1] = 0.0 147 | result.m[5] = 2.0 / tan_angle_height 148 | result.m[9] = (tan_angle_up + tan_angle_down) / tan_angle_height 149 | result.m[13] = 0.0 150 | 151 | result.m[2] = 0.0 152 | result.m[6] = 0.0 153 | result.m[10] = -(far_z + offset_z) / (far_z - near_z) 154 | result.m[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z) 155 | 156 | result.m[3] = 0.0 157 | result.m[7] = 0.0 158 | result.m[11] = -1.0 159 | result.m[15] = 0.0 160 | return result 161 | 162 | @staticmethod 163 | def create_projection_fov(graphics_api: GraphicsAPI, fov: xr.Fovf, near_z: float, far_z: float) -> "Matrix4x4f": 164 | """ Creates a projection matrix based on the specified FOV. """ 165 | tan_left = math.tan(fov.angle_left) 166 | tan_right = math.tan(fov.angle_right) 167 | tan_down = math.tan(fov.angle_down) 168 | tan_up = math.tan(fov.angle_up) 169 | return Matrix4x4f.create_projection(graphics_api, tan_left, tan_right, tan_up, tan_down, near_z, far_z) 170 | 171 | @staticmethod 172 | def create_scale(x: float, y: float, z: float) -> "Matrix4x4f": 173 | """ Creates a scale matrix. """ 174 | result = Matrix4x4f() 175 | result.m[0] = x 176 | result.m[1] = 0.0 177 | result.m[2] = 0.0 178 | result.m[3] = 0.0 179 | result.m[4] = 0.0 180 | result.m[5] = y 181 | result.m[6] = 0.0 182 | result.m[7] = 0.0 183 | result.m[8] = 0.0 184 | result.m[9] = 0.0 185 | result.m[10] = z 186 | result.m[11] = 0.0 187 | result.m[12] = 0.0 188 | result.m[13] = 0.0 189 | result.m[14] = 0.0 190 | result.m[15] = 1.0 191 | return result 192 | 193 | @staticmethod 194 | def create_translation(x: float, y: float, z: float) -> "Matrix4x4f": 195 | """ Creates a translation matrix. """ 196 | result = Matrix4x4f() 197 | result.m[0] = 1.0 198 | result.m[1] = 0.0 199 | result.m[2] = 0.0 200 | result.m[3] = 0.0 201 | result.m[4] = 0.0 202 | result.m[5] = 1.0 203 | result.m[6] = 0.0 204 | result.m[7] = 0.0 205 | result.m[8] = 0.0 206 | result.m[9] = 0.0 207 | result.m[10] = 1.0 208 | result.m[11] = 0.0 209 | result.m[12] = x 210 | result.m[13] = y 211 | result.m[14] = z 212 | result.m[15] = 1.0 213 | return result 214 | 215 | @staticmethod 216 | def create_translation_rotation_scale(translation: xr.Vector3f, rotation: xr.Quaternionf, scale: xr.Vector3f) -> "Matrix4x4f": 217 | """ Creates a combined translation(rotation(scale(object))) matrix. """ 218 | scale_matrix = Matrix4x4f.create_scale(*scale) 219 | rotation_matrix = Matrix4x4f.create_from_quaternion(rotation) 220 | translation_matrix = Matrix4x4f.create_translation(*translation) 221 | combined_matrix = rotation_matrix @ scale_matrix 222 | return translation_matrix @ combined_matrix 223 | 224 | def multiply(self, b: "Matrix4x4f") -> "Matrix4x4f": 225 | """ Use left-multiplication to accumulate transformations. """ 226 | result = Matrix4x4f() 227 | result.m[0] = self.m[0] * b.m[0] + self.m[4] * b.m[1] + self.m[8] * b.m[2] + self.m[12] * b.m[3] 228 | result.m[1] = self.m[1] * b.m[0] + self.m[5] * b.m[1] + self.m[9] * b.m[2] + self.m[13] * b.m[3] 229 | result.m[2] = self.m[2] * b.m[0] + self.m[6] * b.m[1] + self.m[10] * b.m[2] + self.m[14] * b.m[3] 230 | result.m[3] = self.m[3] * b.m[0] + self.m[7] * b.m[1] + self.m[11] * b.m[2] + self.m[15] * b.m[3] 231 | 232 | result.m[4] = self.m[0] * b.m[4] + self.m[4] * b.m[5] + self.m[8] * b.m[6] + self.m[12] * b.m[7] 233 | result.m[5] = self.m[1] * b.m[4] + self.m[5] * b.m[5] + self.m[9] * b.m[6] + self.m[13] * b.m[7] 234 | result.m[6] = self.m[2] * b.m[4] + self.m[6] * b.m[5] + self.m[10] * b.m[6] + self.m[14] * b.m[7] 235 | result.m[7] = self.m[3] * b.m[4] + self.m[7] * b.m[5] + self.m[11] * b.m[6] + self.m[15] * b.m[7] 236 | 237 | result.m[8] = self.m[0] * b.m[8] + self.m[4] * b.m[9] + self.m[8] * b.m[10] + self.m[12] * b.m[11] 238 | result.m[9] = self.m[1] * b.m[8] + self.m[5] * b.m[9] + self.m[9] * b.m[10] + self.m[13] * b.m[11] 239 | result.m[10] = self.m[2] * b.m[8] + self.m[6] * b.m[9] + self.m[10] * b.m[10] + self.m[14] * b.m[11] 240 | result.m[11] = self.m[3] * b.m[8] + self.m[7] * b.m[9] + self.m[11] * b.m[10] + self.m[15] * b.m[11] 241 | 242 | result.m[12] = self.m[0] * b.m[12] + self.m[4] * b.m[13] + self.m[8] * b.m[14] + self.m[12] * b.m[15] 243 | result.m[13] = self.m[1] * b.m[12] + self.m[5] * b.m[13] + self.m[9] * b.m[14] + self.m[13] * b.m[15] 244 | result.m[14] = self.m[2] * b.m[12] + self.m[6] * b.m[13] + self.m[10] * b.m[14] + self.m[14] * b.m[15] 245 | result.m[15] = self.m[3] * b.m[12] + self.m[7] * b.m[13] + self.m[11] * b.m[14] + self.m[15] * b.m[15] 246 | return result 247 | 248 | """ Calculates the inverse of a rigid body transform. """ 249 | def invert_rigid_body(self) -> "Matrix4x4f": 250 | result = Matrix4x4f() 251 | result.m[0] = self.m[0] 252 | result.m[1] = self.m[4] 253 | result.m[2] = self.m[8] 254 | result.m[3] = 0.0 255 | result.m[4] = self.m[1] 256 | result.m[5] = self.m[5] 257 | result.m[6] = self.m[9] 258 | result.m[7] = 0.0 259 | result.m[8] = self.m[2] 260 | result.m[9] = self.m[6] 261 | result.m[10] = self.m[10] 262 | result.m[11] = 0.0 263 | result.m[12] = -(self.m[0] * self.m[12] + self.m[1] * self.m[13] + self.m[2] * self.m[14]) 264 | result.m[13] = -(self.m[4] * self.m[12] + self.m[5] * self.m[13] + self.m[6] * self.m[14]) 265 | result.m[14] = -(self.m[8] * self.m[12] + self.m[9] * self.m[13] + self.m[10] * self.m[14]) 266 | result.m[15] = 1.0 267 | return result 268 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | 5 | import argparse 6 | import logging 7 | import platform 8 | import sys 9 | import threading 10 | import time 11 | 12 | import xr 13 | 14 | from xr_examples.hello_xr.graphics_plugin import IGraphicsPlugin 15 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin 16 | from xr_examples.hello_xr.openxr_program import OpenXRProgram 17 | from xr_examples.hello_xr.graphics_plugin_opengl import OpenGLGraphicsPlugin 18 | from xr_examples.hello_xr.platform_plugin_win32 import Win32PlatformPlugin 19 | from xr_examples.hello_xr.platform_plugin_xlib import XlibPlatformPlugin 20 | from .options import Options 21 | 22 | key_press_event = threading.Event() 23 | logger = logging.getLogger("hello_xr.main") 24 | 25 | 26 | def create_graphics_plugin(options: argparse.Namespace) -> IGraphicsPlugin: 27 | """Create a graphics plugin for the graphics API specified in the options.""" 28 | graphics_plugin_map = { 29 | "OpenGL": OpenGLGraphicsPlugin, 30 | } 31 | if options.graphics_plugin not in graphics_plugin_map: 32 | raise NotImplementedError 33 | return graphics_plugin_map[options.graphics_plugin](options) 34 | 35 | 36 | def create_platform_plugin(_options: argparse.Namespace) -> IPlatformPlugin: 37 | if platform.system() == "Windows": 38 | return Win32PlatformPlugin() 39 | elif platform.system() == "Linux": 40 | return XlibPlatformPlugin() 41 | raise NotImplementedError 42 | 43 | 44 | def poll_keyboard(): 45 | logger.info("Press any key to shutdown...") 46 | try: 47 | sys.stdin.read(1) 48 | key_press_event.set() 49 | logger.debug("A key was pressed") 50 | finally: 51 | pass 52 | 53 | 54 | def main(): 55 | logging.basicConfig( 56 | format="%(asctime)s.%(msecs)03d %(module)s %(levelname)s: %(message)s", 57 | datefmt='%m/%d/%y %I:%M:%S', 58 | level=logging.INFO, 59 | ) 60 | options = Options() 61 | update_options_from_command_line(options) 62 | 63 | # Install keyboard handler to exit on keypress 64 | threading.Thread(target=poll_keyboard, daemon=True).start() 65 | 66 | request_restart = False 67 | while True: 68 | # Create platform-specific implementation. 69 | platform_plugin = create_platform_plugin(options) 70 | # Create graphics API implementation. 71 | with create_graphics_plugin(options) as graphics_plugin, \ 72 | OpenXRProgram(options, platform_plugin, graphics_plugin) as program: 73 | program.create_instance() 74 | program.initialize_system() 75 | 76 | options.set_environment_blend_mode(program.preferred_blend_mode) 77 | update_options_from_command_line(options) 78 | platform_plugin.update_options(options) 79 | graphics_plugin.update_options(options) 80 | 81 | program.initialize_device() 82 | program.initialize_session() 83 | program.create_swapchains() 84 | while not key_press_event.is_set(): 85 | # glfw notices when you click the close button 86 | exit_render_loop = graphics_plugin.poll_events() 87 | if exit_render_loop: 88 | break 89 | exit_render_loop, request_restart = program.poll_events() 90 | if exit_render_loop: 91 | break 92 | if program.session_running: 93 | try: 94 | program.poll_actions() 95 | except xr.exception.SessionNotFocused: 96 | # TODO: C++ code does not need this conditional. Why does python? 97 | pass 98 | program.render_frame() 99 | else: 100 | # Throttle loop since xrWaitFrame won't be called. 101 | time.sleep(0.250) 102 | if key_press_event.is_set() or not request_restart: 103 | break 104 | 105 | 106 | def update_options_from_command_line(options: Options) -> bool: 107 | parser = argparse.ArgumentParser() 108 | parser.add_argument( 109 | "--graphics", "-g", 110 | choices=["D3D11", "D3D12", "OpenGLES", "OpenGL", "Vulkan2", "Vulkan"], 111 | ) 112 | parser.add_argument( 113 | "--formfactor", "-ff", 114 | choices=["Hmd", "Handheld"], 115 | ) 116 | parser.add_argument( 117 | "--viewconfig", "-vc", 118 | choices=["Mono", "Stereo"], 119 | ) 120 | parser.add_argument( 121 | "--blendmode", "-bm", 122 | choices=["Opaque", "Additive", "AlphaBlend"], 123 | ) 124 | parser.add_argument( 125 | "--space", "-s", 126 | choices=["View", "Local", "Stage"], 127 | ) 128 | parser.add_argument( 129 | "--verbose", "-v", action="store_true", 130 | ) 131 | parsed = parser.parse_args() 132 | if parsed.verbose: 133 | logging.getLogger("hello_xr").setLevel(logging.DEBUG) 134 | if parsed.graphics is not None: 135 | options.graphics_plugin = parsed.graphics 136 | if parsed.formfactor is not None: 137 | options.form_factor = parsed.formfactor 138 | if parsed.viewconfig is not None: 139 | options.view_configuration = parsed.viewconfig 140 | if parsed.blendmode is not None: 141 | options.environment_blend_mode = parsed.blendmode 142 | if parsed.space is not None: 143 | options.app_space = parsed.space 144 | options.parse_strings() 145 | return True 146 | 147 | 148 | if __name__ == "__main__": 149 | main() 150 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/openxr_program.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | 5 | import argparse 6 | import ctypes 7 | from ctypes import ( 8 | addressof, 9 | byref, 10 | c_float, 11 | c_int32, 12 | c_void_p, 13 | cast, 14 | pointer, 15 | POINTER, 16 | Structure, 17 | ) 18 | import enum 19 | import logging 20 | import math 21 | import platform 22 | from typing import List, Optional 23 | 24 | import xr.raw_functions 25 | 26 | from .graphics_plugin import Cube, IGraphicsPlugin 27 | from .platform_plugin import IPlatformPlugin 28 | from .options import Options 29 | 30 | logger = logging.getLogger("hello_xr.program") 31 | 32 | 33 | class Math(object): 34 | class Pose(object): 35 | @staticmethod 36 | def identity(): 37 | t = xr.Posef() 38 | assert t.orientation.w == 1 39 | return t 40 | 41 | @staticmethod 42 | def translation(translation: List[float]): 43 | t = Math.Pose.identity() 44 | t.position[:] = translation[:] 45 | return t 46 | 47 | @staticmethod 48 | def rotate_ccw_about_y_axis(radians: float, translation: List[float]): 49 | t = Math.Pose.identity() 50 | t.orientation.x = 0 51 | t.orientation.y = math.sin(radians * 0.5) 52 | t.orientation.z = 0 53 | t.orientation.w = math.cos(radians * 0.5) 54 | t.position[:] = translation[:] 55 | return t 56 | 57 | 58 | class Side(enum.IntEnum): 59 | LEFT = 0 60 | RIGHT = 1 61 | 62 | 63 | class Swapchain(Structure): 64 | _fields_ = [ 65 | ("handle", xr.Swapchain), 66 | ("width", c_int32), 67 | ("height", c_int32), 68 | ] 69 | 70 | 71 | class OpenXRProgram(object): 72 | def __init__(self, options: argparse.Namespace, platform_plugin: IPlatformPlugin, graphics_plugin: IGraphicsPlugin): 73 | self.options = options 74 | self.platform_plugin = platform_plugin 75 | self.graphics_plugin = graphics_plugin 76 | 77 | self.debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback) 78 | 79 | self.instance = None 80 | self.session = None 81 | self.app_space = None 82 | self.form_factor = xr.FormFactor.HEAD_MOUNTED_DISPLAY 83 | self.system = None # Higher level System class, not just ID 84 | 85 | self.config_views = [] 86 | self.swapchains = [] 87 | self.swapchain_image_buffers = [] # to keep objects alive 88 | self.swapchain_image_ptr_buffers = {} # m_swapchainImages 89 | self.views = (xr.View * 2)(xr.View(), xr.View()) 90 | self.color_swapchain_format = -1 91 | 92 | self.visualized_spaces = [] 93 | 94 | # Application's current lifecycle state according to the runtime 95 | self.session_state = xr.SessionState.UNKNOWN 96 | self.session_running = False 97 | 98 | self.event_data_buffer = xr.EventDataBuffer() 99 | self.input = OpenXRProgram.InputState() 100 | 101 | self.acceptable_blend_modes = [ 102 | xr.EnvironmentBlendMode.OPAQUE, 103 | xr.EnvironmentBlendMode.ADDITIVE, 104 | xr.EnvironmentBlendMode.ALPHA_BLEND, 105 | ] 106 | 107 | def __enter__(self): 108 | return self 109 | 110 | def __exit__(self, exc_type, exc_val, exc_tb): 111 | if self.input.action_set is not None: 112 | for hand in Side: 113 | if self.input.hand_space[hand] is not None: 114 | xr.destroy_space(self.input.hand_space[hand]) 115 | self.input.hand_space[hand] = None 116 | xr.destroy_action_set(self.input.action_set) 117 | self.input.action_set = None 118 | for swapchain in self.swapchains: 119 | xr.destroy_swapchain(swapchain.handle) 120 | self.swapchains[:] = [] 121 | for visualized_space in self.visualized_spaces: 122 | xr.destroy_space(visualized_space) 123 | self.visualized_spaces[:] = [] 124 | if self.app_space is not None: 125 | xr.destroy_space(self.app_space) 126 | self.app_space = None 127 | if self.session is not None: 128 | xr.destroy_session(self.session) 129 | self.session = None 130 | if self.instance is not None: 131 | # Workaround for bug https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422 132 | if platform.system() != "Linux": 133 | self.instance.destroy() 134 | self.instance = None 135 | 136 | def create_instance(self) -> None: 137 | """Create an Instance and other basic instance-level initialization.""" 138 | self.log_layers_and_extensions() 139 | self.create_instance_internal() 140 | self.log_instance_info() 141 | 142 | def create_instance_internal(self): 143 | assert self.instance is None 144 | # Create union of extensions required by platform and graphics plugins. 145 | extensions = [] 146 | # Enable debug messaging 147 | discovered_extensions = xr.enumerate_instance_extension_properties() 148 | dumci = xr.DebugUtilsMessengerCreateInfoEXT() 149 | next_structure = self.platform_plugin.instance_create_extension 150 | ALL_SEVERITIES = ( 151 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 152 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 153 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT 154 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT 155 | ) 156 | 157 | ALL_TYPES = ( 158 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 159 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 160 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 161 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT 162 | ) 163 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in discovered_extensions: 164 | extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME) 165 | dumci.message_severities = ALL_SEVERITIES 166 | dumci.message_types = ALL_TYPES 167 | dumci.user_data = None 168 | dumci.user_callback = self.debug_callback 169 | if next_structure is None: 170 | next_structure = cast(pointer(dumci), c_void_p) 171 | else: 172 | next_structure.next = cast(pointer(dumci), c_void_p) 173 | # 174 | extensions.extend(self.platform_plugin.instance_extensions) 175 | extensions.extend(self.graphics_plugin.instance_extensions) 176 | 177 | self.instance = xr.InstanceObject( 178 | enabled_extensions=extensions, 179 | application_name="hello_xr.py", 180 | application_version=xr.Version(0, 0, 1), 181 | # Current version is 1.1.x, but hello_xr only requires 1.0.x 182 | api_version=xr.XR_API_VERSION_1_0, 183 | next=next_structure, 184 | ) 185 | 186 | def create_swapchains(self) -> None: 187 | """ 188 | Create a Swapchain which requires coordinating with the graphics plugin to select the format, getting the system graphics 189 | properties, getting the view configuration and grabbing the resulting swapchain images. 190 | """ 191 | assert self.session is not None 192 | assert len(self.swapchains) == 0 193 | assert len(self.config_views) == 0 194 | # Read graphics properties for preferred swapchain length and logging. 195 | system_properties = xr.get_system_properties(self.instance.handle, self.system.id) 196 | # Log system properties 197 | logger.info("System Properties: " 198 | f"Name={system_properties.system_name.decode()} " 199 | f"VendorId={system_properties.vendor_id}") 200 | logger.info("System Graphics Properties: " 201 | f"MaxWidth={system_properties.graphics_properties.max_swapchain_image_width} " 202 | f"MaxHeight={system_properties.graphics_properties.max_swapchain_image_height} " 203 | f"MaxLayers={system_properties.graphics_properties.max_layer_count}") 204 | logger.info("System Tracking Properties: " 205 | f"OrientationTracking={bool(system_properties.tracking_properties.orientation_tracking)} " 206 | f"PositionTracking={bool(system_properties.tracking_properties.position_tracking)}") 207 | # Note: No other view configurations exist at the time this (C++) code was written. If this 208 | # condition is not met, the project will need to be audited to see how support should be 209 | # added. 210 | if self.options.parsed["view_config_type"] != xr.ViewConfigurationType.PRIMARY_STEREO: 211 | raise RuntimeError("Unsupported view configuration type") 212 | # Query and cache view configuration views. 213 | self.config_views = xr.enumerate_view_configuration_views( 214 | instance=self.instance.handle, 215 | system_id=self.system.id, 216 | view_configuration_type=self.options.parsed["view_config_type"], 217 | ) 218 | # Create and cache view buffer for xrLocateViews later. 219 | view_count = len(self.config_views) 220 | assert view_count == 2 221 | self.views = (xr.View * view_count)(*([xr.View()] * view_count)) 222 | # Create the swapchain and get the images. 223 | if view_count > 0: 224 | # Select a swapchain format. 225 | swapchain_formats = xr.enumerate_swapchain_formats(self.session) 226 | self.color_swapchain_format = self.graphics_plugin.select_color_swapchain_format(swapchain_formats) 227 | # Print swapchain formats and the selected one. 228 | formats_string = "" 229 | for sc_format in swapchain_formats: 230 | selected = sc_format == self.color_swapchain_format 231 | formats_string += " " 232 | if selected: 233 | formats_string += "[" 234 | formats_string += f"{str(self.color_swapchain_format)}({sc_format})" 235 | formats_string += "]" 236 | else: 237 | formats_string += str(sc_format) 238 | logger.debug(f"Swapchain Formats: {formats_string}") 239 | # Create a swapchain for each view. 240 | for i, vp in enumerate(self.config_views): 241 | logger.info("Creating swapchain for " 242 | f"view {i} with dimensions " 243 | f"Width={vp.recommended_image_rect_width} " 244 | f"Height={vp.recommended_image_rect_height} " 245 | f"SampleCount={vp.recommended_swapchain_sample_count}") 246 | # Create the swapchain. 247 | swapchain_create_info = xr.SwapchainCreateInfo( 248 | array_size=1, 249 | format=self.color_swapchain_format, 250 | width=vp.recommended_image_rect_width, 251 | height=vp.recommended_image_rect_height, 252 | mip_count=1, 253 | face_count=1, 254 | sample_count=self.graphics_plugin.get_supported_swapchain_sample_count(vp), 255 | usage_flags=xr.SwapchainUsageFlags.SAMPLED_BIT | xr.SwapchainUsageFlags.COLOR_ATTACHMENT_BIT, 256 | ) 257 | swapchain = Swapchain( 258 | xr.create_swapchain( 259 | session=self.session, 260 | create_info=swapchain_create_info, 261 | ), 262 | swapchain_create_info.width, 263 | swapchain_create_info.height, 264 | ) 265 | self.swapchains.append(swapchain) 266 | swapchain_image_buffer = xr.enumerate_swapchain_images( 267 | swapchain=swapchain.handle, 268 | element_type=self.graphics_plugin.swapchain_image_type, 269 | ) 270 | # Keep the buffer alive by moving it into the list of buffers. 271 | self.swapchain_image_buffers.append(swapchain_image_buffer) 272 | capacity = len(swapchain_image_buffer) 273 | swapchain_image_ptr_buffer = (POINTER(xr.SwapchainImageBaseHeader) * capacity)() 274 | for ix in range(capacity): 275 | swapchain_image_ptr_buffer[ix] = cast( 276 | byref(swapchain_image_buffer[ix]), 277 | POINTER(xr.SwapchainImageBaseHeader)) 278 | self.swapchain_image_ptr_buffers[handle_key(swapchain.handle)] = swapchain_image_ptr_buffer 279 | 280 | def create_visualized_spaces(self): 281 | assert self.session is not None 282 | visualized_spaces = [ 283 | "ViewFront", "Local", "Stage", "StageLeft", "StageRight", 284 | "StageLeftRotated", "StageRightRotated", 285 | ] 286 | for visualized_space in visualized_spaces: 287 | try: 288 | space = xr.create_reference_space( 289 | session=self.session, 290 | create_info=get_xr_reference_space_create_info(visualized_space) 291 | ) 292 | self.visualized_spaces.append(space) 293 | except xr.XrException as exc: 294 | logger.warning(f"Failed to create reference space {visualized_space} with error {exc}") 295 | 296 | def handle_session_state_changed_event( 297 | self, state_changed_event, exit_render_loop, request_restart) -> (bool, bool): 298 | # TODO: avoid this ugly cast 299 | event = cast(byref(state_changed_event), POINTER(xr.EventDataSessionStateChanged)).contents 300 | old_state = self.session_state 301 | self.session_state = xr.SessionState(event.state) 302 | key = cast(self.session, c_void_p).value 303 | logger.info(f"XrEventDataSessionStateChanged: " 304 | f"state {str(old_state)}->{str(self.session_state)} " 305 | f"session={hex(key)} time={event.time}") 306 | if event.session is not None and handle_key(event.session) != handle_key(self.session): 307 | logger.error(f"XrEventDataSessionStateChanged for unknown session {event.session} {self.session}") 308 | return exit_render_loop, request_restart 309 | 310 | if self.session_state == xr.SessionState.READY: 311 | assert self.session is not None 312 | xr.begin_session( 313 | session=self.session, 314 | begin_info=xr.SessionBeginInfo( 315 | primary_view_configuration_type=self.options.parsed["view_config_type"], 316 | ), 317 | ) 318 | self.session_running = True 319 | elif self.session_state == xr.SessionState.STOPPING: 320 | assert self.session is not None 321 | self.session_running = False 322 | xr.end_session(self.session) 323 | elif self.session_state == xr.SessionState.EXITING: 324 | exit_render_loop = True 325 | # Do not attempt to restart because user closed this session. 326 | request_restart = False 327 | elif self.session_state == xr.SessionState.LOSS_PENDING: 328 | exit_render_loop = True 329 | # Poll for a new instance. 330 | request_restart = True 331 | return exit_render_loop, request_restart 332 | 333 | def initialize_actions(self): 334 | # Create an action set. 335 | action_set_info = xr.ActionSetCreateInfo( 336 | action_set_name="gameplay", 337 | localized_action_set_name="Gameplay", 338 | priority=0, 339 | ) 340 | self.input.action_set = xr.create_action_set(self.instance.handle, action_set_info) 341 | # Get the XrPath for the left and right hands - we will use them as subaction paths. 342 | self.input.hand_subaction_path[Side.LEFT] = xr.string_to_path( 343 | self.instance.handle, 344 | "/user/hand/left") 345 | self.input.hand_subaction_path[Side.RIGHT] = xr.string_to_path( 346 | self.instance.handle, 347 | "/user/hand/right") 348 | # Create actions 349 | # Create an input action for grabbing objects with the left and right hands. 350 | self.input.grab_action = xr.create_action( 351 | action_set=self.input.action_set, 352 | create_info=xr.ActionCreateInfo( 353 | action_type=xr.ActionType.FLOAT_INPUT, 354 | action_name="grab_object", 355 | localized_action_name="Grab Object", 356 | count_subaction_paths=len(self.input.hand_subaction_path), 357 | subaction_paths=self.input.hand_subaction_path, 358 | ), 359 | ) 360 | # Create an input action getting the left and right hand poses. 361 | self.input.pose_action = xr.create_action( 362 | action_set=self.input.action_set, 363 | create_info=xr.ActionCreateInfo( 364 | action_type=xr.ActionType.POSE_INPUT, 365 | action_name="hand_pose", 366 | localized_action_name="Hand Pose", 367 | count_subaction_paths=len(self.input.hand_subaction_path), 368 | subaction_paths=self.input.hand_subaction_path, 369 | ), 370 | ) 371 | # Create output actions for vibrating the left and right controller. 372 | self.input.vibrate_action = xr.create_action( 373 | action_set=self.input.action_set, 374 | create_info=xr.ActionCreateInfo( 375 | action_type=xr.ActionType.VIBRATION_OUTPUT, 376 | action_name="vibrate_hand", 377 | localized_action_name="Vibrate Hand", 378 | count_subaction_paths=len(self.input.hand_subaction_path), 379 | subaction_paths=self.input.hand_subaction_path, 380 | ), 381 | ) 382 | # Create input actions for quitting the session using the left and right controller. 383 | # Since it doesn't matter which hand did this, we do not specify subaction paths for it. 384 | # We will just suggest bindings for both hands, where possible. 385 | self.input.quit_action = xr.create_action( 386 | action_set=self.input.action_set, 387 | create_info=xr.ActionCreateInfo( 388 | action_type=xr.ActionType.BOOLEAN_INPUT, 389 | action_name="quit_session", 390 | localized_action_name="Quit Session", 391 | count_subaction_paths=0, 392 | subaction_paths=None, 393 | ), 394 | ) 395 | select_path = [ 396 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/select/click"), 397 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/select/click")] 398 | _squeeze_value_path = [ 399 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/value"), 400 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/value")] 401 | _squeeze_force_path = [ 402 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/force"), 403 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/force")] 404 | _squeeze_click_path = [ 405 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/click"), 406 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/click")] 407 | pose_path = [ 408 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/grip/pose"), 409 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/grip/pose")] 410 | haptic_path = [ 411 | xr.string_to_path(self.instance.handle, "/user/hand/left/output/haptic"), 412 | xr.string_to_path(self.instance.handle, "/user/hand/right/output/haptic")] 413 | menu_click_path = [ 414 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/menu/click"), 415 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/menu/click")] 416 | _b_click_path = [ 417 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/b/click"), 418 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/b/click")] 419 | trigger_value_path = [ 420 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/trigger/value"), 421 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/trigger/value")] 422 | # Suggest bindings for KHR Simple. 423 | khr_bindings = [ 424 | # Fall back to a click input for the grab action. 425 | xr.ActionSuggestedBinding(self.input.grab_action, select_path[Side.LEFT]), 426 | xr.ActionSuggestedBinding(self.input.grab_action, select_path[Side.RIGHT]), 427 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.LEFT]), 428 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.RIGHT]), 429 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.LEFT]), 430 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.RIGHT]), 431 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.LEFT]), 432 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.RIGHT]), 433 | ] 434 | xr.suggest_interaction_profile_bindings( 435 | instance=self.instance.handle, 436 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 437 | interaction_profile=xr.string_to_path( 438 | self.instance.handle, 439 | "/interaction_profiles/khr/simple_controller", 440 | ), 441 | count_suggested_bindings=len(khr_bindings), 442 | suggested_bindings=(xr.ActionSuggestedBinding * len(khr_bindings))(*khr_bindings), 443 | ), 444 | ) 445 | # Suggest bindings for the Vive Controller. 446 | vive_bindings = [ 447 | xr.ActionSuggestedBinding(self.input.grab_action, trigger_value_path[Side.LEFT]), 448 | xr.ActionSuggestedBinding(self.input.grab_action, trigger_value_path[Side.RIGHT]), 449 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.LEFT]), 450 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.RIGHT]), 451 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.LEFT]), 452 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.RIGHT]), 453 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.LEFT]), 454 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.RIGHT]), 455 | ] 456 | xr.suggest_interaction_profile_bindings( 457 | instance=self.instance.handle, 458 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 459 | interaction_profile=xr.string_to_path( 460 | self.instance.handle, 461 | "/interaction_profiles/htc/vive_controller", 462 | ), 463 | count_suggested_bindings=len(vive_bindings), 464 | suggested_bindings=(xr.ActionSuggestedBinding * len(vive_bindings))(*vive_bindings), 465 | ), 466 | ) 467 | # TODO the other controller types in openxr_programs.cpp 468 | 469 | action_space_info = xr.ActionSpaceCreateInfo( 470 | action=self.input.pose_action, 471 | # pose_in_action_space # w already defaults to 1 in python... 472 | subaction_path=self.input.hand_subaction_path[Side.LEFT], 473 | ) 474 | assert action_space_info.pose_in_action_space.orientation.w == 1 475 | self.input.hand_space[Side.LEFT] = xr.create_action_space( 476 | session=self.session, 477 | create_info=action_space_info, 478 | ) 479 | action_space_info.subaction_path = self.input.hand_subaction_path[Side.RIGHT] 480 | self.input.hand_space[Side.RIGHT] = xr.create_action_space( 481 | session=self.session, 482 | create_info=action_space_info, 483 | ) 484 | xr.attach_session_action_sets( 485 | session=self.session, 486 | attach_info=xr.SessionActionSetsAttachInfo( 487 | count_action_sets=1, 488 | action_sets=pointer(self.input.action_set), 489 | ), 490 | ) 491 | 492 | def initialize_session(self) -> None: 493 | """Create a Session and other basic session-level initialization.""" 494 | assert self.instance is not None 495 | assert self.instance.handle != xr.NULL_HANDLE 496 | assert self.session is None 497 | logger.debug(f"Creating session...") 498 | graphics_binding_pointer = cast( 499 | pointer(self.graphics_plugin.graphics_binding), 500 | c_void_p) 501 | create_info = xr.SessionCreateInfo( 502 | next=graphics_binding_pointer, 503 | system_id=self.system.id, 504 | ) 505 | self.session = xr.create_session( 506 | instance=self.instance.handle, 507 | create_info=create_info, 508 | ) 509 | self.log_reference_spaces() 510 | self.initialize_actions() 511 | self.create_visualized_spaces() 512 | self.app_space = xr.create_reference_space( 513 | session=self.session, 514 | create_info=get_xr_reference_space_create_info(self.options.app_space), 515 | ) 516 | 517 | def initialize_system(self) -> None: 518 | """ 519 | Select a System for the view configuration specified in the Options and initialize the graphics device for the selected 520 | system. 521 | """ 522 | assert self.instance is not None 523 | assert self.system is None 524 | form_factor = Options.get_xr_form_factor(self.options.form_factor) 525 | self.system = xr.SystemObject(instance=self.instance, form_factor=form_factor) 526 | logger.debug(f"Using system {hex(self.system.id.value)} for form factor {str(form_factor)}") 527 | assert self.instance.handle is not None 528 | assert self.system.id is not None 529 | 530 | def initialize_device(self): 531 | self.log_view_configurations() 532 | # The graphics API can initialize the graphics device now that the systemId and instance 533 | # handle are available. 534 | self.graphics_plugin.initialize_device(self.instance.handle, self.system.id) 535 | 536 | def log_action_source_name(self, action: xr.Action, action_name: str): 537 | paths = xr.enumerate_bound_sources_for_action( 538 | session=self.session, 539 | enumerate_info=xr.BoundSourcesForActionEnumerateInfo( 540 | action=action, 541 | ), 542 | ) 543 | source_name = "" 544 | for path in paths: 545 | all_flags = xr.INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT \ 546 | | xr.INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT \ 547 | | xr.INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT 548 | grab_source = xr.get_input_source_localized_name( 549 | session=self.session, 550 | get_info=xr.InputSourceLocalizedNameGetInfo( 551 | source_path=path, 552 | which_components=all_flags, 553 | ), 554 | ) 555 | if len(grab_source) < 1: 556 | continue 557 | if len(source_name) > 0: 558 | source_name += " and " 559 | source_name += f"'{grab_source}'" 560 | logger.info(f"{action_name} is bound to {source_name if len(source_name) > 0 else 'nothing'}") 561 | 562 | def log_environment_blend_mode(self, view_config_type): 563 | assert self.instance.handle is not None 564 | assert self.system.id is not None 565 | blend_modes = xr.enumerate_environment_blend_modes(self.instance.handle, self.system.id, view_config_type) 566 | logger.info(f"Available Environment Blend Mode count : ({len(blend_modes)})") 567 | blend_mode_found = False 568 | for mode_value in blend_modes: 569 | mode = xr.EnvironmentBlendMode(mode_value) 570 | blend_mode_match = mode == self.options.parsed["environment_blend_mode"] 571 | logger.info(f"Environment Blend Mode ({str(mode)}) : " 572 | f"{'(Selected)' if blend_mode_match else ''}") 573 | blend_mode_found |= blend_mode_match 574 | assert blend_mode_found 575 | 576 | def log_instance_info(self): 577 | assert self.instance is not None 578 | assert self.instance.handle is not None 579 | instance_properties = self.instance.get_properties() 580 | logger.info( 581 | f"Instance RuntimeName={instance_properties.runtime_name.decode()} " 582 | f"RuntimeVersion={xr.Version(instance_properties.runtime_version)}") 583 | 584 | def log_layers_and_extensions(self): 585 | # Log non-api_layer extensions 586 | self._log_extensions(layer_name=None) 587 | # Log layers and any of their extensions 588 | layers = xr.enumerate_api_layer_properties() 589 | logger.info(f"Available Layers: ({len(layers)})") 590 | for layer in layers: 591 | logger.debug( 592 | f" Name={layer.layer_name.decode()} " 593 | f"SpecVersion={self.xr_version_string()} " 594 | f"LayerVersion={layer.layer_version} " 595 | f"Description={layer.description.decode()}") 596 | self._log_extensions(layer_name=layer.layer_name.decode(), indent=4) 597 | 598 | def log_reference_spaces(self): 599 | assert self.session is not None 600 | spaces = xr.enumerate_reference_spaces(self.session) 601 | logger.info(f"Available reference spaces: {len(spaces)}") 602 | for space in spaces: 603 | logger.debug(f" Name: {str(xr.ReferenceSpaceType(space))}") 604 | 605 | def log_view_configurations(self): 606 | assert self.instance.handle is not None 607 | assert self.system.id is not None 608 | view_config_types = xr.enumerate_view_configurations(self.instance.handle, self.system.id) 609 | logger.info(f"Available View Configuration Types: ({len(view_config_types)})") 610 | for view_config_type_value in view_config_types: 611 | view_config_type = xr.ViewConfigurationType(view_config_type_value) 612 | logger.debug( 613 | f" View Configuration Type: {str(view_config_type)} " 614 | f"{'(Selected)' if view_config_type == self.options.parsed['view_config_type'] else ''}") 615 | view_config_properties = xr.get_view_configuration_properties( 616 | instance=self.instance.handle, 617 | system_id=self.system.id, 618 | view_configuration_type=view_config_type, 619 | ) 620 | logger.debug(f" View configuration FovMutable={bool(view_config_properties.fov_mutable)}") 621 | configuration_views = xr.enumerate_view_configuration_views(self.instance.handle, self.system.id, 622 | view_config_type) 623 | if configuration_views is None or len(configuration_views) < 1: 624 | logger.error(f"Empty view configuration type") 625 | else: 626 | for i, view in enumerate(configuration_views): 627 | logger.debug( 628 | f" View [{i}]: Recommended Width={view.recommended_image_rect_width} " 629 | f"Height={view.recommended_image_rect_height} " 630 | f"SampleCount={view.recommended_swapchain_sample_count}") 631 | logger.debug( 632 | f" View [{i}]: Maximum Width={view.max_image_rect_width} " 633 | f"Height={view.max_image_rect_height} " 634 | f"SampleCount={view.max_swapchain_sample_count}") 635 | self.log_environment_blend_mode(view_config_type) 636 | 637 | @staticmethod 638 | def _log_extensions(layer_name, indent: int = 0): 639 | """Write out extension properties for a given api_layer.""" 640 | extension_properties = xr.enumerate_instance_extension_properties(layer_name) 641 | indent_str = " " * indent 642 | logger.debug(f"{indent_str}Available Extensions ({len(extension_properties)})") 643 | for extension in extension_properties: 644 | logger.debug( 645 | f"{indent_str} Name={extension.extension_name.decode()} SpecVersion={extension.extension_version}") 646 | 647 | def poll_actions(self) -> None: 648 | """Sample input actions and generate haptic feedback.""" 649 | self.input.hand_active[:] = [xr.FALSE, xr.FALSE] 650 | # Sync actions 651 | active_action_set = xr.ActiveActionSet(self.input.action_set, xr.NULL_PATH) 652 | xr.sync_actions( 653 | self.session, 654 | xr.ActionsSyncInfo( 655 | count_active_action_sets=1, 656 | active_action_sets=pointer(active_action_set) 657 | ), 658 | ) 659 | # Get pose and grab action state and start haptic vibrate when hand is 90% squeezed. 660 | for hand in Side: 661 | grab_value = xr.get_action_state_float( 662 | self.session, 663 | xr.ActionStateGetInfo( 664 | action=self.input.grab_action, 665 | subaction_path=self.input.hand_subaction_path[hand], 666 | ), 667 | ) 668 | if grab_value.is_active: 669 | # Scale the rendered hand by 1.0f (open) to 0.5f (fully squeezed). 670 | self.input.hand_scale[hand] = 1 - 0.5 * grab_value.current_state 671 | if grab_value.current_state > 0.9: 672 | vibration = xr.HapticVibration( 673 | amplitude=0.5, 674 | duration=xr.MIN_HAPTIC_DURATION, 675 | frequency=xr.FREQUENCY_UNSPECIFIED, 676 | ) 677 | xr.apply_haptic_feedback( 678 | session=self.session, 679 | haptic_action_info=xr.HapticActionInfo( 680 | action=self.input.vibrate_action, 681 | subaction_path=self.input.hand_subaction_path[hand], 682 | ), 683 | haptic_feedback=cast(byref(vibration), POINTER(xr.HapticBaseHeader)).contents, 684 | ) 685 | pose_state = xr.get_action_state_pose( 686 | session=self.session, 687 | get_info=xr.ActionStateGetInfo( 688 | action=self.input.pose_action, 689 | subaction_path=self.input.hand_subaction_path[hand], 690 | ), 691 | ) 692 | self.input.hand_active[hand] = pose_state.is_active 693 | # There were no subaction paths specified for the quit action, because we don't care which hand did it. 694 | quit_value = xr.get_action_state_boolean( 695 | session=self.session, 696 | get_info=xr.ActionStateGetInfo( 697 | action=self.input.quit_action, 698 | subaction_path=xr.NULL_PATH, 699 | ), 700 | ) 701 | if quit_value.is_active and quit_value.changed_since_last_sync and quit_value.current_state: 702 | xr.request_exit_session(self.session) 703 | 704 | def poll_events(self) -> (bool, bool): 705 | """Process any events in the event queue.""" 706 | exit_render_loop = False 707 | request_restart = False 708 | # Process all pending messages. 709 | while True: 710 | event = self.try_read_next_event() 711 | if event is None: 712 | break 713 | event_type = event.type 714 | if event_type == xr.StructureType.EVENT_DATA_INSTANCE_LOSS_PENDING: 715 | logger.warning(f"XrEventDataInstanceLossPending by {event.loss_time}") 716 | return True, True 717 | elif event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED: 718 | exit_render_loop, request_restart = self.handle_session_state_changed_event( 719 | event, exit_render_loop, request_restart) 720 | elif event_type == xr.StructureType.EVENT_DATA_INTERACTION_PROFILE_CHANGED: 721 | self.log_action_source_name(self.input.grab_action, "Grab") 722 | self.log_action_source_name(self.input.quit_action, "Quit") 723 | self.log_action_source_name(self.input.pose_action, "Pose") 724 | self.log_action_source_name(self.input.vibrate_action, "Vibrate") 725 | elif event_type == xr.StructureType.EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: 726 | logger.debug(f"Ignoring event type {str(event_type)}") 727 | else: 728 | logger.debug(f"Ignoring event type {str(event_type)}") 729 | return exit_render_loop, request_restart 730 | 731 | @property 732 | def preferred_blend_mode(self): 733 | blend_modes = xr.enumerate_environment_blend_modes(self.instance.handle, self.system.id, self.options.parsed["view_config_type"]) 734 | for blend_mode in blend_modes: 735 | if blend_mode in self.acceptable_blend_modes: 736 | return blend_mode 737 | raise RuntimeError("No acceptable blend mode returned from the xrEnumerateEnvironmentBlendModes") 738 | 739 | def render_frame(self) -> None: 740 | """Create and submit a frame.""" 741 | assert self.session is not None 742 | frame_state = xr.wait_frame( 743 | session=self.session, 744 | frame_wait_info=xr.FrameWaitInfo(), 745 | ) 746 | xr.begin_frame(self.session, xr.FrameBeginInfo()) 747 | 748 | layers = [] 749 | layer_flags = 0 750 | if self.options.environment_blend_mode == "AlphaBlend": 751 | layer_flags = xr.COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT \ 752 | | xr.COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT 753 | layer = xr.CompositionLayerProjection( 754 | space=self.app_space, 755 | layer_flags=layer_flags, 756 | ) 757 | projection_layer_views = (xr.CompositionLayerProjectionView * 2)( 758 | xr.CompositionLayerProjectionView(), 759 | xr.CompositionLayerProjectionView()) 760 | if frame_state.should_render: 761 | if self.render_layer(frame_state.predicted_display_time, projection_layer_views, layer): 762 | layers.append(byref(layer)) 763 | 764 | xr.end_frame( 765 | session=self.session, 766 | frame_end_info=xr.FrameEndInfo( 767 | display_time=frame_state.predicted_display_time, 768 | environment_blend_mode=self.options.parsed["environment_blend_mode"], 769 | layers=layers, 770 | ), 771 | ) 772 | 773 | def render_layer( 774 | self, 775 | predicted_display_time: xr.Time, 776 | projection_layer_views: ctypes.Array, 777 | layer: xr.CompositionLayerProjection, 778 | ) -> bool: 779 | view_capacity_input = len(self.views) 780 | view_state, self.views = xr.locate_views( 781 | session=self.session, 782 | view_locate_info=xr.ViewLocateInfo( 783 | view_configuration_type=self.options.parsed["view_config_type"], 784 | display_time=predicted_display_time, 785 | space=self.app_space, 786 | ), 787 | ) 788 | view_count_output = len(self.views) 789 | vsf = view_state.view_state_flags 790 | if (vsf & xr.VIEW_STATE_POSITION_VALID_BIT == 0 791 | or vsf & xr.VIEW_STATE_ORIENTATION_VALID_BIT == 0): 792 | return False # There are no valid tracking poses for the views. 793 | assert view_count_output == view_capacity_input 794 | assert view_count_output == len(self.config_views) 795 | assert view_count_output == len(self.swapchains) 796 | assert view_count_output == len(projection_layer_views) 797 | 798 | # For each locatable space that we want to visualize, render a 25cm cube. 799 | cubes = [] 800 | for visualized_space in self.visualized_spaces: 801 | # TODO: sometimes xr.locate_space() raises xr.exception.TimeInvalidError 802 | # Maybe try skipping a few frames instead of crashing 803 | space_location = xr.locate_space( 804 | space=visualized_space, 805 | base_space=self.app_space, 806 | time=predicted_display_time, 807 | ) 808 | loc_flags = space_location.location_flags 809 | if (loc_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT != 0 810 | and loc_flags & xr.SPACE_LOCATION_ORIENTATION_VALID_BIT != 0): 811 | cubes.append(Cube(space_location.pose, xr.Vector3f(0.25, 0.25, 0.25))) 812 | # Render a 10cm cube scaled by grabAction for each hand. Note renderHand will only be 813 | # true when the application has focus. 814 | for hand in Side: 815 | space_location = xr.locate_space( 816 | space=self.input.hand_space[hand], 817 | base_space=self.app_space, 818 | time=predicted_display_time, 819 | ) 820 | loc_flags = space_location.location_flags 821 | if (loc_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT != 0 822 | and loc_flags & xr.SPACE_LOCATION_ORIENTATION_VALID_BIT != 0): 823 | scale = 0.1 * self.input.hand_scale[hand] 824 | cubes.append(Cube(space_location.pose, xr.Vector3f(scale, scale, scale))) 825 | # Render view to the appropriate part of the swapchain image. 826 | for i in range(view_count_output): 827 | view_swapchain = self.swapchains[i] 828 | swapchain_image_index = xr.acquire_swapchain_image( 829 | swapchain=view_swapchain.handle, 830 | acquire_info=xr.SwapchainImageAcquireInfo(), 831 | ) 832 | xr.wait_swapchain_image( 833 | swapchain=view_swapchain.handle, 834 | wait_info=xr.SwapchainImageWaitInfo(timeout=xr.INFINITE_DURATION), 835 | ) 836 | view = projection_layer_views[i] 837 | assert view.type == xr.StructureType.COMPOSITION_LAYER_PROJECTION_VIEW 838 | view.pose = self.views[i].pose 839 | view.fov = self.views[i].fov 840 | view.sub_image.swapchain = view_swapchain.handle 841 | view.sub_image.image_rect.offset[:] = [0, 0] 842 | view.sub_image.image_rect.extent[:] = [ 843 | view_swapchain.width, view_swapchain.height, ] 844 | swapchain_image_ptr = self.swapchain_image_ptr_buffers[handle_key(view_swapchain.handle)][swapchain_image_index] 845 | self.graphics_plugin.render_view( 846 | view, 847 | swapchain_image_ptr, 848 | self.color_swapchain_format, 849 | cubes, 850 | mirror=i == Side.LEFT, # mirror left eye only 851 | # mirror=False, 852 | ) 853 | xr.release_swapchain_image( 854 | swapchain=view_swapchain.handle, 855 | release_info=xr.SwapchainImageReleaseInfo() 856 | ) 857 | layer.views = projection_layer_views 858 | return True 859 | 860 | def try_read_next_event(self) -> Optional[Structure]: 861 | # It is sufficient to clear the just the XrEventDataBuffer header to 862 | # XR_TYPE_EVENT_DATA_BUFFER 863 | base_header = self.event_data_buffer 864 | base_header.type = xr.StructureType.EVENT_DATA_BUFFER 865 | result = xr.raw_functions.xrPollEvent(self.instance.handle, byref(self.event_data_buffer)) 866 | if result == xr.Result.SUCCESS: 867 | if base_header.type == xr.StructureType.EVENT_DATA_EVENTS_LOST: 868 | events_lost = cast(base_header, POINTER(xr.EventDataEventsLost)) 869 | logger.warning(f"{events_lost} events lost") 870 | return base_header 871 | if result == xr.Result.EVENT_UNAVAILABLE: 872 | return None 873 | result2 = xr.check_result(result) 874 | raise result2 875 | 876 | @staticmethod 877 | def xr_version_string(): 878 | return xr.XR_CURRENT_API_VERSION 879 | 880 | class InputState(Structure): 881 | def __init__(self): 882 | super().__init__() 883 | self.hand_scale[:] = [1, 1] 884 | 885 | _fields_ = [ 886 | ("action_set", xr.ActionSet), 887 | ("grab_action", xr.Action), 888 | ("pose_action", xr.Action), 889 | ("vibrate_action", xr.Action), 890 | ("quit_action", xr.Action), 891 | ("hand_subaction_path", xr.Path * len(Side)), 892 | ("hand_space", xr.Space * len(Side)), 893 | ("hand_scale", c_float * len(Side)), 894 | ("hand_active", xr.Bool32 * len(Side)), 895 | ] 896 | 897 | 898 | def get_xr_reference_space_create_info(reference_space_type_string: str) -> xr.ReferenceSpaceCreateInfo: 899 | create_info = xr.ReferenceSpaceCreateInfo( 900 | pose_in_reference_space=Math.Pose.identity(), 901 | ) 902 | space_type = reference_space_type_string.lower() 903 | if space_type == "View".lower(): 904 | create_info.reference_space_type = xr.ReferenceSpaceType.VIEW 905 | elif space_type == "ViewFront".lower(): 906 | # Render head-locked 2m in front of device. 907 | create_info.pose_in_reference_space = Math.Pose.translation(translation=[0, 0, -2]) 908 | create_info.reference_space_type = xr.ReferenceSpaceType.VIEW 909 | elif space_type == "Local".lower(): 910 | create_info.reference_space_type = xr.ReferenceSpaceType.LOCAL 911 | elif space_type == "Stage".lower(): 912 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE 913 | elif space_type == "StageLeft".lower(): 914 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE 915 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(0, [-2, 0, -2]) 916 | elif space_type == "StageRight".lower(): 917 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE 918 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(0, [2, 0, -2]) 919 | elif space_type == "StageLeftRotated".lower(): 920 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE 921 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(math.pi / 3, [-2, 0.5, -2]) 922 | elif space_type == "StageRightRotated".lower(): 923 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE 924 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(-math.pi / 3, [2, 0.5, -2]) 925 | else: 926 | raise ValueError(f"Unknown reference space type '{reference_space_type_string}'") 927 | return create_info 928 | 929 | 930 | def handle_key(handle): 931 | return hex(cast(handle, c_void_p).value) 932 | 933 | 934 | def openxr_log_level(severity_flags: int) -> int: 935 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: 936 | return logging.ERROR 937 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: 938 | return logging.WARNING 939 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: 940 | return logging.INFO 941 | else: 942 | return logging.DEBUG 943 | 944 | 945 | def xr_debug_callback( 946 | severity: xr.DebugUtilsMessageSeverityFlagsEXT, 947 | _type: xr.DebugUtilsMessageTypeFlagsEXT, 948 | data: POINTER(xr.DebugUtilsMessengerCallbackDataEXT), 949 | _user_data: c_void_p) -> bool: 950 | d = data.contents 951 | # TODO structure properties to return unicode strings 952 | logger.log(openxr_log_level(severity), f"OpenXR: {d.function_name.decode()}: {d.message.decode()}") 953 | return True 954 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/options.py: -------------------------------------------------------------------------------- 1 | import xr 2 | 3 | 4 | class Options(object): 5 | def __init__(self): 6 | self.graphics_plugin = "OpenGL" 7 | self.form_factor = "Hmd" 8 | self.view_configuration = "Stereo" 9 | self.environment_blend_mode = "Opaque" 10 | self.app_space = "Local" 11 | self.parsed = { 12 | "form_factor": xr.FormFactor.HEAD_MOUNTED_DISPLAY, 13 | "view_config_type": xr.ViewConfigurationType.PRIMARY_STEREO, 14 | "environment_blend_mode": xr.EnvironmentBlendMode.OPAQUE, 15 | } 16 | 17 | @property 18 | def background_clear_color(self) -> tuple: 19 | slate_grey = (0.184313729, 0.309803933, 0.309803933, 1.0) 20 | if self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.OPAQUE: 21 | return slate_grey # SlateGrey 22 | elif self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.ADDITIVE: 23 | return 0, 0, 0, 1 # Black 24 | elif self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.ALPHA_BLEND: 25 | return 0, 0, 0, 0 # TransparentBlack 26 | else: 27 | return slate_grey 28 | 29 | @staticmethod 30 | def get_xr_environment_blend_mode(environment_blend_mode_string: str) -> xr.EnvironmentBlendMode: 31 | return { 32 | "Opaque": xr.EnvironmentBlendMode.OPAQUE, 33 | "Additive": xr.EnvironmentBlendMode.ADDITIVE, 34 | "AlphaBlend": xr.EnvironmentBlendMode.ALPHA_BLEND, 35 | }[environment_blend_mode_string] 36 | 37 | @staticmethod 38 | def get_xr_environment_blend_mode_string(environment_blend_mode: xr.EnvironmentBlendMode) -> str: 39 | return { 40 | xr.EnvironmentBlendMode.OPAQUE: "Opaque", 41 | xr.EnvironmentBlendMode.ADDITIVE: "Additive", 42 | xr.EnvironmentBlendMode.ALPHA_BLEND: "AlphaBlend", 43 | }[environment_blend_mode] 44 | 45 | @staticmethod 46 | def get_xr_form_factor(form_factor_string: str) -> xr.FormFactor: 47 | if form_factor_string == "Hmd": 48 | return xr.FormFactor.HEAD_MOUNTED_DISPLAY 49 | elif form_factor_string == "Handheld": 50 | return xr.FormFactor.HANDHELD_DISPLAY 51 | raise ValueError(f"Unknown form factor '{form_factor_string}'") 52 | 53 | @staticmethod 54 | def get_xr_view_configuration_type(view_configuration_string: str) -> xr.ViewConfigurationType: 55 | if view_configuration_string == "Mono": 56 | return xr.ViewConfigurationType.PRIMARY_MONO 57 | elif view_configuration_string == "Stereo": 58 | return xr.ViewConfigurationType.PRIMARY_STEREO 59 | raise ValueError(f"Unknown view configuration '{view_configuration_string}'") 60 | 61 | def parse_strings(self): 62 | self.parsed["form_factor"] = self.get_xr_form_factor(self.form_factor) 63 | self.parsed["view_config_type"] = self.get_xr_view_configuration_type(self.view_configuration) 64 | self.parsed["environment_blend_mode"] = self.get_xr_environment_blend_mode(self.environment_blend_mode) 65 | 66 | def set_environment_blend_mode(self, environment_blend_mode: xr.EnvironmentBlendMode) -> None: 67 | self.environment_blend_mode = self.get_xr_environment_blend_mode_string(environment_blend_mode) 68 | self.parsed["environment_blend_mode"] = environment_blend_mode 69 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/platform_plugin.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from ctypes import Structure 3 | from typing import List, Optional 4 | 5 | 6 | class IPlatformPlugin(abc.ABC): 7 | """Wraps platform-specific implementation so the main openxr program can be platform-independent.""" 8 | 9 | @abc.abstractmethod 10 | def __enter__(self): 11 | pass 12 | 13 | @abc.abstractmethod 14 | def __exit__(self, exc_type, exc_val, exc_tb): 15 | """Clean up platform resources.""" 16 | 17 | @property 18 | @abc.abstractmethod 19 | def instance_create_extension(self) -> Optional[Structure]: 20 | """OpenXR instance-level extensions required by this platform.""" 21 | 22 | @property 23 | @abc.abstractmethod 24 | def instance_extensions(self) -> List[str]: 25 | """OpenXR instance-level extensions required by this platform.""" 26 | 27 | @abc.abstractmethod 28 | def update_options(self, options) -> None: 29 | pass 30 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/platform_plugin_win32.py: -------------------------------------------------------------------------------- 1 | from ctypes import Structure 2 | from typing import Optional 3 | 4 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin 5 | 6 | 7 | class Win32PlatformPlugin(IPlatformPlugin): 8 | def __enter__(self): 9 | return self 10 | 11 | def __exit__(self, exc_type, exc_val, exc_tb): 12 | pass 13 | 14 | @property 15 | def instance_create_extension(self) -> Optional[Structure]: 16 | return None 17 | 18 | @property 19 | def instance_extensions(self): 20 | return [] 21 | 22 | def update_options(self, options) -> None: 23 | pass 24 | -------------------------------------------------------------------------------- /xr_examples/hello_xr/platform_plugin_xlib.py: -------------------------------------------------------------------------------- 1 | from ctypes import Structure 2 | from typing import Optional 3 | 4 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin 5 | 6 | 7 | class XlibPlatformPlugin(IPlatformPlugin): 8 | def __enter__(self): 9 | return self 10 | 11 | def __exit__(self, exc_type, exc_val, exc_tb): 12 | pass 13 | 14 | @property 15 | def instance_create_extension(self) -> Optional[Structure]: 16 | return None 17 | 18 | @property 19 | def instance_extensions(self): 20 | return [] 21 | 22 | def update_options(self, options) -> None: 23 | pass 24 | -------------------------------------------------------------------------------- /xr_examples/openxr_version.py: -------------------------------------------------------------------------------- 1 | import xr 2 | 3 | print(f"OpenXR version {xr.version.XR_CURRENT_API_VERSION}") 4 | -------------------------------------------------------------------------------- /xr_examples/pink_world.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program pink_world.py 3 | This example renders a solid pink field to each eye. 4 | """ 5 | 6 | from OpenGL import GL 7 | import xr 8 | 9 | 10 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 11 | with xr.ContextObject( 12 | instance_create_info=xr.InstanceCreateInfo( 13 | enabled_extension_names=[ 14 | # A graphics extension is mandatory (without a headless extension) 15 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 16 | ], 17 | ), 18 | ) as context: 19 | for frame_index, frame_state in enumerate(context.frame_loop()): 20 | for view in context.view_loop(frame_state): 21 | GL.glClearColor(1, 0.7, 0.7, 1) # pink 22 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 23 | if frame_index > 500: # Don't run forever 24 | break 25 | -------------------------------------------------------------------------------- /xr_examples/runtime_name.py: -------------------------------------------------------------------------------- 1 | import xr 2 | 3 | instance = xr.create_instance(create_info=xr.InstanceCreateInfo()) 4 | instance_props = xr.get_instance_properties(instance) 5 | print(f"The current active OpenXR runtime is: {instance_props.runtime_name.decode()}") 6 | -------------------------------------------------------------------------------- /xr_examples/track_controller.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program track_controller.py 3 | 4 | Prints the position of your right-hand controller for 30 frames. 5 | """ 6 | 7 | import ctypes 8 | import time 9 | import xr 10 | 11 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 12 | with xr.ContextObject( 13 | instance_create_info=xr.InstanceCreateInfo( 14 | enabled_extension_names=[ 15 | # A graphics extension is mandatory (without a headless extension) 16 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 17 | ], 18 | ), 19 | ) as context: 20 | # Set up the controller pose action 21 | controller_paths = (xr.Path * 2)( 22 | xr.string_to_path(context.instance, "/user/hand/left"), 23 | xr.string_to_path(context.instance, "/user/hand/right"), 24 | ) 25 | controller_pose_action = xr.create_action( 26 | action_set=context.default_action_set, 27 | create_info=xr.ActionCreateInfo( 28 | action_type=xr.ActionType.POSE_INPUT, 29 | action_name="hand_pose", 30 | localized_action_name="Hand Pose", 31 | count_subaction_paths=len(controller_paths), 32 | subaction_paths=controller_paths, 33 | ), 34 | ) 35 | suggested_bindings = (xr.ActionSuggestedBinding * 2)( 36 | xr.ActionSuggestedBinding( 37 | action=controller_pose_action, 38 | binding=xr.string_to_path( 39 | instance=context.instance, 40 | path_string="/user/hand/left/input/grip/pose", 41 | ), 42 | ), 43 | xr.ActionSuggestedBinding( 44 | action=controller_pose_action, 45 | binding=xr.string_to_path( 46 | instance=context.instance, 47 | path_string="/user/hand/right/input/grip/pose", 48 | ), 49 | ), 50 | ) 51 | xr.suggest_interaction_profile_bindings( 52 | instance=context.instance, 53 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 54 | interaction_profile=xr.string_to_path( 55 | context.instance, 56 | "/interaction_profiles/khr/simple_controller", 57 | ), 58 | count_suggested_bindings=len(suggested_bindings), 59 | suggested_bindings=suggested_bindings, 60 | ), 61 | ) 62 | xr.suggest_interaction_profile_bindings( 63 | instance=context.instance, 64 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 65 | interaction_profile=xr.string_to_path( 66 | context.instance, 67 | "/interaction_profiles/htc/vive_controller", 68 | ), 69 | count_suggested_bindings=len(suggested_bindings), 70 | suggested_bindings=suggested_bindings, 71 | ), 72 | ) 73 | 74 | action_spaces = [ 75 | xr.create_action_space( 76 | session=context.session, 77 | create_info=xr.ActionSpaceCreateInfo( 78 | action=controller_pose_action, 79 | subaction_path=controller_paths[0], 80 | ), 81 | ), 82 | xr.create_action_space( 83 | session=context.session, 84 | create_info=xr.ActionSpaceCreateInfo( 85 | action=controller_pose_action, 86 | subaction_path=controller_paths[1], 87 | ), 88 | ), 89 | ] 90 | # Loop over the render frames 91 | session_was_focused = False # Check for a common problem 92 | for frame_index, frame_state in enumerate(context.frame_loop()): 93 | 94 | if context.session_state == xr.SessionState.FOCUSED: 95 | session_was_focused = True 96 | active_action_set = xr.ActiveActionSet( 97 | action_set=context.default_action_set, 98 | subaction_path=xr.NULL_PATH, 99 | ) 100 | xr.sync_actions( 101 | session=context.session, 102 | sync_info=xr.ActionsSyncInfo( 103 | count_active_action_sets=1, 104 | active_action_sets=ctypes.pointer(active_action_set), 105 | ), 106 | ) 107 | found_count = 0 108 | for index, space in enumerate(action_spaces): 109 | space_location = xr.locate_space( 110 | space=space, 111 | base_space=context.space, 112 | time=frame_state.predicted_display_time, 113 | ) 114 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: 115 | print(index + 1, space_location.pose) 116 | found_count += 1 117 | if found_count == 0: 118 | print("no controllers active") 119 | 120 | # Slow things down, especially since we are not rendering anything 121 | time.sleep(0.5) 122 | # Don't run forever 123 | if frame_index > 30: 124 | break 125 | if not session_was_focused: 126 | print("This OpenXR session never entered the FOCUSED state. Did you wear the headset?") 127 | -------------------------------------------------------------------------------- /xr_examples/track_hmd.py: -------------------------------------------------------------------------------- 1 | import time 2 | import xr 3 | 4 | # Once XR_KHR_headless extension is ratified and adopted, we 5 | # should be able to avoid the Window and frame stuff here. 6 | with xr.InstanceObject(application_name="track_hmd") as instance, \ 7 | xr.SystemObject(instance) as system, \ 8 | xr.GlfwWindow(system) as window, \ 9 | xr.SessionObject(system, graphics_binding=window.graphics_binding) as session: 10 | for _ in range(50): 11 | session.poll_xr_events() 12 | if session.state in ( 13 | xr.SessionState.READY, 14 | xr.SessionState.SYNCHRONIZED, 15 | xr.SessionState.VISIBLE, 16 | xr.SessionState.FOCUSED, 17 | ): 18 | session.wait_frame() 19 | session.begin_frame() 20 | view_state, views = session.locate_views() 21 | print(views[xr.Eye.LEFT.value].pose, flush=True) 22 | time.sleep(0.5) 23 | session.end_frame() 24 | -------------------------------------------------------------------------------- /xr_examples/track_hmd2.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program track_hmd2.py 3 | 4 | Prints the position of your head-mounted display for 30 frames. 5 | """ 6 | 7 | import time 8 | import xr 9 | 10 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 11 | with xr.ContextObject( 12 | instance_create_info=xr.InstanceCreateInfo( 13 | enabled_extension_names=[ 14 | # A graphics extension is mandatory (without a headless extension) 15 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 16 | ], 17 | ), 18 | ) as context: 19 | # Loop over the render frames 20 | for frame_index, frame_state in enumerate(context.frame_loop()): 21 | view_state, views = xr.locate_views( 22 | session=context.session, 23 | view_locate_info=xr.ViewLocateInfo( 24 | view_configuration_type=context.view_configuration_type, 25 | display_time=frame_state.predicted_display_time, 26 | space=context.space, 27 | ) 28 | ) 29 | flags = xr.ViewStateFlags(view_state.view_state_flags) 30 | if flags & xr.ViewStateFlags.POSITION_VALID_BIT: 31 | view = views[xr.Eye.LEFT] 32 | print(view.pose, flush=True) 33 | else: 34 | print("pose not valid") 35 | # Slow things down, especially since we are not rendering anything 36 | time.sleep(0.5) 37 | # Don't run forever 38 | if frame_index > 30: 39 | break 40 | -------------------------------------------------------------------------------- /xr_examples/vive_tracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyopenxr example program vive_tracker.py 3 | 4 | Prints the position and orientation of your vive trackers each frame. 5 | 6 | Helpful instructions for getting trackers working on Linux are at 7 | https://gist.github.com/DanielArnett/c9a56c9c7cc0def20648480bca1f6772 8 | The udev symbolic link trick was crucial in my case. 9 | """ 10 | 11 | import ctypes 12 | from ctypes import cast, byref 13 | import time 14 | import xr 15 | 16 | print("Warning: trackers with role 'Handheld object' won't be detected.") 17 | 18 | # ContextObject is a high level pythonic class meant to keep simple cases simple. 19 | with xr.ContextObject( 20 | instance_create_info=xr.InstanceCreateInfo( 21 | enabled_extension_names=[ 22 | # A graphics extension is mandatory (without a headless extension) 23 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, 24 | xr.extension.HTCX_vive_tracker_interaction.NAME, 25 | ], 26 | ), 27 | ) as context: 28 | instance = context.instance 29 | session = context.session 30 | 31 | # Save the function pointer 32 | enumerateViveTrackerPathsHTCX = cast( 33 | xr.get_instance_proc_addr( 34 | instance, 35 | "xrEnumerateViveTrackerPathsHTCX", 36 | ), 37 | xr.PFN_xrEnumerateViveTrackerPathsHTCX 38 | ) 39 | 40 | # Create the action with subaction path 41 | # Role strings from 42 | # https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#XR_HTCX_vive_tracker_interaction 43 | role_strings = [ 44 | "handheld_object", 45 | "left_foot", 46 | "right_foot", 47 | "left_shoulder", 48 | "right_shoulder", 49 | "left_elbow", 50 | "right_elbow", 51 | "left_knee", 52 | "right_knee", 53 | "waist", 54 | "chest", 55 | "camera", 56 | "keyboard", 57 | ] 58 | role_path_strings = [f"/user/vive_tracker_htcx/role/{role}" 59 | for role in role_strings] 60 | role_paths = (xr.Path * len(role_path_strings))( 61 | *[xr.string_to_path(instance, role_string) for role_string in role_path_strings], 62 | ) 63 | pose_action = xr.create_action( 64 | action_set=context.default_action_set, 65 | create_info=xr.ActionCreateInfo( 66 | action_type=xr.ActionType.POSE_INPUT, 67 | action_name="tracker_pose", 68 | localized_action_name="Tracker Pose", 69 | count_subaction_paths=len(role_paths), 70 | subaction_paths=role_paths, 71 | ), 72 | ) 73 | # Describe a suggested binding for that action and subaction path 74 | suggested_binding_paths = (xr.ActionSuggestedBinding * len(role_path_strings))( 75 | *[xr.ActionSuggestedBinding( 76 | pose_action, 77 | xr.string_to_path(instance, f"{role_path_string}/input/grip/pose")) 78 | for role_path_string in role_path_strings], 79 | ) 80 | xr.suggest_interaction_profile_bindings( 81 | instance=instance, 82 | suggested_bindings=xr.InteractionProfileSuggestedBinding( 83 | interaction_profile=xr.string_to_path(instance, "/interaction_profiles/htc/vive_tracker_htcx"), 84 | count_suggested_bindings=len(suggested_binding_paths), 85 | suggested_bindings=suggested_binding_paths, 86 | ) 87 | ) 88 | # Create action spaces for locating trackers in each role 89 | tracker_action_spaces = (xr.Space * len(role_paths))( 90 | *[xr.create_action_space( 91 | session=session, 92 | create_info=xr.ActionSpaceCreateInfo( 93 | action=pose_action, 94 | subaction_path=role_path, 95 | ) 96 | ) for role_path in role_paths], 97 | ) 98 | 99 | n_paths = ctypes.c_uint32(0) 100 | result = enumerateViveTrackerPathsHTCX(instance, 0, byref(n_paths), None) 101 | if xr.check_result(result).is_exception(): 102 | raise result 103 | vive_tracker_paths = (xr.ViveTrackerPathsHTCX * n_paths.value)(*([xr.ViveTrackerPathsHTCX()] * n_paths.value)) 104 | # print(xr.Result(result), n_paths.value) 105 | result = enumerateViveTrackerPathsHTCX(instance, n_paths, byref(n_paths), vive_tracker_paths) 106 | if xr.check_result(result).is_exception(): 107 | raise result 108 | print(xr.Result(result), n_paths.value) 109 | # print(*vive_tracker_paths) 110 | 111 | # Loop over the render frames 112 | session_was_focused = False # Check for a common problem 113 | for frame_index, frame_state in enumerate(context.frame_loop()): 114 | 115 | if context.session_state == xr.SessionState.FOCUSED: 116 | session_was_focused = True 117 | active_action_set = xr.ActiveActionSet( 118 | action_set=context.default_action_set, 119 | subaction_path=xr.NULL_PATH, 120 | ) 121 | xr.sync_actions( 122 | session=session, 123 | sync_info=xr.ActionsSyncInfo( 124 | count_active_action_sets=1, 125 | active_action_sets=ctypes.pointer(active_action_set), 126 | ), 127 | ) 128 | 129 | n_paths = ctypes.c_uint32(0) 130 | result = enumerateViveTrackerPathsHTCX(instance, 0, byref(n_paths), None) 131 | if xr.check_result(result).is_exception(): 132 | raise result 133 | vive_tracker_paths = (xr.ViveTrackerPathsHTCX * n_paths.value)(*([xr.ViveTrackerPathsHTCX()] * n_paths.value)) 134 | # print(xr.Result(result), n_paths.value) 135 | result = enumerateViveTrackerPathsHTCX(instance, n_paths, byref(n_paths), vive_tracker_paths) 136 | if xr.check_result(result).is_exception(): 137 | raise result 138 | # print(xr.Result(result), n_paths.value) 139 | # print(*vive_tracker_paths) 140 | 141 | found_tracker_count = 0 142 | for index, space in enumerate(tracker_action_spaces): 143 | space_location = xr.locate_space( 144 | space=space, 145 | base_space=context.space, 146 | time=frame_state.predicted_display_time, 147 | ) 148 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: 149 | print(f"{role_strings[index]}: {space_location.pose}") 150 | found_tracker_count += 1 151 | if found_tracker_count == 0: 152 | print("no trackers found") 153 | 154 | # Slow things down, especially since we are not rendering anything 155 | time.sleep(0.5) 156 | # Don't run forever 157 | if frame_index > 30: 158 | break 159 | if not session_was_focused: 160 | print("This OpenXR session never entered the FOCUSED state. Did you wear the headset?") 161 | -------------------------------------------------------------------------------- /xr_examples/where_does_it_hang.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a work in progress... 3 | 4 | The goal here is to create the simplest case to demonstrate the hang that occurs 5 | in xr.destroy_instance() on Linux with SteamVR. 6 | 7 | The hang does not occur on Windows; it *does* occur on linux. 8 | The hang does not occur when calling xr.destroy_instance() without out any frame loop calls. 9 | it *does* occur when calling xr.destroy_instance after running a frame loop. 10 | """ 11 | 12 | import ctypes 13 | from ctypes import byref, c_int32, c_void_p, cast, POINTER, pointer, Structure 14 | import logging # 1) Use the python logging system 15 | import os 16 | import platform 17 | import time 18 | 19 | if platform.system() == "Windows": 20 | from OpenGL import WGL 21 | elif platform.system() == "Linux": 22 | from OpenGL import GLX 23 | from OpenGL import GL 24 | import xr 25 | 26 | 27 | run_frame_loop = True 28 | 29 | 30 | class Swapchain(Structure): 31 | _fields_ = [ 32 | ("handle", xr.Swapchain), 33 | ("width", c_int32), 34 | ("height", c_int32), 35 | ] 36 | 37 | 38 | # 2) Hook into the pyopenxr logger hierarchy. 39 | logging.basicConfig() # You might want also to study the parameters to basicConfig()... 40 | pyopenxr_logger = logging.getLogger("pyopenxr") 41 | # show me ALL the messages 42 | pyopenxr_logger.setLevel(logging.INFO) # Modify argument according to your current needs 43 | 44 | # 3) Create a child logger for this particular program 45 | logger = logging.getLogger("pyopenxr.where_does_it_hang") 46 | logger.info("Hey, this logging thing works!") # Test it out 47 | 48 | # Use API layers for debugging 49 | enabled_api_layers = [] 50 | 51 | # 4) Core validation adds additional messages about correct use of the OpenXR api 52 | if xr.LUNARG_core_validation_APILAYER_NAME in xr.enumerate_api_layer_properties(): 53 | enabled_api_layers.append(xr.LUNARG_core_validation_APILAYER_NAME) 54 | 55 | # 5) API dump shows the details of every OpenXR call. Use this for deep debugging only. 56 | if False and xr.LUNARG_api_dump_APILAYER_NAME in xr.enumerate_api_layer_properties(): 57 | enabled_api_layers.append(xr.LUNARG_api_dump_APILAYER_NAME) 58 | os.environ["XR_API_DUMP_EXPORT_TYPE"] = "text" # or "html" 59 | # os.environ["XR_API_DUMP_FILE_NAME"] = "/some/file/name" 60 | 61 | # Use extensions for debugging 62 | enabled_extensions = [] 63 | # 6) XR_EXT_debug_utils can be used to redirect pyopenxr debugging messages to our logger, 64 | # among other things. 65 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties(): 66 | enabled_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME) 67 | 68 | 69 | # Define helper function for our logging callback 70 | def openxr_log_level(severity_flags: xr.DebugUtilsMessageSeverityFlagsEXT) -> int: 71 | """Convert OpenXR message severities to python logging severities.""" 72 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: 73 | return logging.ERROR 74 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: 75 | return logging.WARNING 76 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: 77 | return logging.INFO 78 | else: 79 | return logging.DEBUG 80 | 81 | 82 | def xr_debug_callback( 83 | severity: xr.DebugUtilsMessageSeverityFlagsEXT, 84 | _type: xr.DebugUtilsMessageTypeFlagsEXT, 85 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT), 86 | _user_data: ctypes.c_void_p) -> bool: 87 | """Redirect OpenXR messages to our python logger.""" 88 | d = data.contents 89 | pyopenxr_logger.log( 90 | level=openxr_log_level(severity), 91 | msg=f"OpenXR: {d.function_name.decode()}: {d.message.decode()}") 92 | return True 93 | 94 | 95 | # Prepare to create a debug utils messenger 96 | pfn_xr_debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback) 97 | debug_utils_messenger_create_info = xr.DebugUtilsMessengerCreateInfoEXT( 98 | message_severities=( 99 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 100 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 101 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT 102 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT 103 | ), 104 | message_types=( 105 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 106 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 107 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 108 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT 109 | ), 110 | user_callback=pfn_xr_debug_callback, 111 | ) 112 | 113 | ptr_debug_messenger = None 114 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties(): 115 | # Insert our debug_utils_messenger create_info 116 | ptr_debug_messenger = ctypes.cast( 117 | ctypes.pointer(debug_utils_messenger_create_info), ctypes.c_void_p) 118 | 119 | # 7) Turn on extra debugging messages in the OpenXR Loader 120 | os.environ["XR_LOADER_DEBUG"] = "all" 121 | os.environ["LD_BIND_NOW"] = "1" 122 | 123 | 124 | # Create OpenXR instance with attached layers, extensions, and debug messenger. 125 | enabled_extensions.append(xr.KHR_OPENGL_ENABLE_EXTENSION_NAME) 126 | instance = xr.create_instance( 127 | xr.InstanceCreateInfo( 128 | enabled_api_layer_names=enabled_api_layers, 129 | enabled_extension_names=enabled_extensions, 130 | next=ptr_debug_messenger, 131 | ) 132 | ) 133 | 134 | system_id = xr.get_system( 135 | instance=instance, 136 | get_info=xr.SystemGetInfo( 137 | form_factor=xr.FormFactor.HEAD_MOUNTED_DISPLAY, 138 | ), 139 | ) 140 | 141 | graphics = xr.OpenGLGraphics( 142 | instance=instance, 143 | system=system_id, 144 | title="Horatio Hornblower", 145 | ) 146 | 147 | # OpenGL can report debug messages too 148 | # Create a sub-logger for messages from OpenGL 149 | gl_logger = logging.getLogger("pyopenxr.opengl") 150 | 151 | 152 | def gl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user): 153 | """Redirect OpenGL debug messages""" 154 | log_level = { 155 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR, 156 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING, 157 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO, 158 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG, 159 | }[severity] 160 | gl_logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}") 161 | 162 | 163 | gl_debug_message_proc = GL.GLDEBUGPROC(gl_debug_message_callback) 164 | # We need an active OpenGL context before calling glDebugMessageCallback 165 | graphics.make_current() 166 | GL.glDebugMessageCallback(gl_debug_message_proc, None) 167 | 168 | graphics_binding_pointer = cast(pointer(graphics.graphics_binding), c_void_p) 169 | session = xr.create_session( 170 | instance=instance, 171 | create_info=xr.SessionCreateInfo( 172 | system_id=system_id, 173 | next=graphics_binding_pointer, 174 | ), 175 | ) 176 | 177 | space = xr.create_reference_space( 178 | session=session, 179 | create_info=xr.ReferenceSpaceCreateInfo(), 180 | ) 181 | 182 | default_action_set = xr.create_action_set( 183 | instance=instance, 184 | create_info=xr.ActionSetCreateInfo( 185 | action_set_name="default_action_set", 186 | localized_action_set_name="Default Action Set", 187 | priority=0, 188 | ), 189 | ) 190 | 191 | # Create swapchains 192 | view_configuration_type = xr.ViewConfigurationType.PRIMARY_STEREO 193 | config_views = xr.enumerate_view_configuration_views( 194 | instance=instance, 195 | system_id=system_id, 196 | view_configuration_type=view_configuration_type, 197 | ) 198 | graphics.initialize_resources() 199 | swapchain_formats = xr.enumerate_swapchain_formats(session) 200 | color_swapchain_format = graphics.select_color_swapchain_format(swapchain_formats) 201 | # Create a swapchain for each view. 202 | swapchains = [] 203 | swapchain_image_buffers = [] 204 | swapchain_image_ptr_buffers = [] 205 | for vp in config_views: 206 | # Create the swapchain. 207 | swapchain_create_info = xr.SwapchainCreateInfo( 208 | array_size=1, 209 | format=color_swapchain_format, 210 | width=vp.recommended_image_rect_width, 211 | height=vp.recommended_image_rect_height, 212 | mip_count=1, 213 | face_count=1, 214 | sample_count=vp.recommended_swapchain_sample_count, 215 | usage_flags=xr.SwapchainUsageFlags.SAMPLED_BIT | xr.SwapchainUsageFlags.COLOR_ATTACHMENT_BIT, 216 | ) 217 | swapchain = Swapchain( 218 | xr.create_swapchain( 219 | session=session, 220 | create_info=swapchain_create_info, 221 | ), 222 | swapchain_create_info.width, 223 | swapchain_create_info.height, 224 | ) 225 | swapchains.append(swapchain) 226 | swapchain_image_buffer = xr.enumerate_swapchain_images( 227 | swapchain=swapchain.handle, 228 | element_type=graphics.swapchain_image_type, 229 | ) 230 | # Keep the buffer alive by moving it into the list of buffers. 231 | swapchain_image_buffers.append(swapchain_image_buffer) 232 | capacity = len(swapchain_image_buffer) 233 | swapchain_image_ptr_buffer = (POINTER(xr.SwapchainImageBaseHeader) * capacity)() 234 | for ix in range(capacity): 235 | swapchain_image_ptr_buffer[ix] = cast( 236 | byref(swapchain_image_buffer[ix]), 237 | POINTER(xr.SwapchainImageBaseHeader)) 238 | swapchain_image_ptr_buffers.append(swapchain_image_ptr_buffer) 239 | graphics.make_current() 240 | # frame_loop 241 | action_sets = [default_action_set,] 242 | xr.attach_session_action_sets( 243 | session=session, 244 | attach_info=xr.SessionActionSetsAttachInfo( 245 | count_action_sets=len(action_sets), 246 | action_sets=(xr.ActionSet * len(action_sets))( 247 | *action_sets 248 | ) 249 | ), 250 | ) 251 | session_is_running = False 252 | frame_count = 0 253 | 254 | if run_frame_loop: 255 | while True: 256 | frame_count += 1 257 | window_closed = graphics.poll_events() 258 | if window_closed: 259 | xr.request_exit_session(session) 260 | # poll_xr_events 261 | exit_render_loop = False 262 | while True: 263 | try: 264 | event_buffer = xr.poll_event(instance) 265 | event_type = xr.StructureType(event_buffer.type) 266 | if event_type == xr.StructureType.EVENT_DATA_INSTANCE_LOSS_PENDING: 267 | # still handle rest of the events instead of immediately quitting 268 | pass 269 | elif event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED \ 270 | and session is not None: 271 | event = cast( 272 | byref(event_buffer), 273 | POINTER(xr.EventDataSessionStateChanged)).contents 274 | session_state = xr.SessionState(event.state) 275 | logger.info(f"Session state changed to {str(session_state)}") 276 | if session_state == xr.SessionState.IDLE: 277 | pass 278 | elif session_state == xr.SessionState.READY: 279 | xr.begin_session( 280 | session=session, 281 | begin_info=xr.SessionBeginInfo( 282 | view_configuration_type, 283 | ), 284 | ) 285 | session_is_running = True 286 | elif session_state == xr.SessionState.STOPPING: 287 | session_is_running = False 288 | xr.end_session(session) 289 | elif session_state == xr.SessionState.EXITING: 290 | exit_render_loop = True 291 | elif session_state == xr.SessionState.LOSS_PENDING: 292 | exit_render_loop = True 293 | elif event_type == xr.StructureType.EVENT_DATA_VIVE_TRACKER_CONNECTED_HTCX: 294 | vive_tracker_connected = cast(byref(event_buffer), POINTER(xr.EventDataViveTrackerConnectedHTCX)).contents 295 | paths = vive_tracker_connected.paths.contents 296 | persistent_path_str = xr.path_to_string(instance, paths.persistent_path) 297 | # print(f"Vive Tracker connected: {persistent_path_str}") 298 | if paths.role_path != xr.NULL_PATH: 299 | role_path_str = xr.path_to_string(instance, paths.role_path) 300 | # print(f" New role is: {role_path_str}") 301 | else: 302 | # print(f" No role path.") 303 | pass 304 | elif event_type == xr.StructureType.EVENT_DATA_INTERACTION_PROFILE_CHANGED: 305 | # print("data interaction profile changed") 306 | # TODO: 307 | pass 308 | except xr.EventUnavailable: 309 | break 310 | # end of poll_xr_events 311 | if exit_render_loop: 312 | break 313 | if session_is_running: 314 | if session_state in ( 315 | xr.SessionState.READY, 316 | xr.SessionState.SYNCHRONIZED, 317 | xr.SessionState.VISIBLE, 318 | xr.SessionState.FOCUSED, 319 | ): 320 | # xr.request_exit_session(session) # Request exit here allows clean exit 321 | # Something about begin/wait/end_frame is making it hang at the end. 322 | frame_state = xr.wait_frame(session, xr.FrameWaitInfo()) 323 | xr.begin_frame(session, xr.FrameBeginInfo()) 324 | render_layers = [] 325 | if frame_state.should_render: 326 | graphics.make_current() 327 | GL.glClearColor(1, 0.6, 0.6, 1) 328 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 329 | time.sleep(0.02) 330 | xr.end_frame( 331 | session, 332 | frame_end_info=xr.FrameEndInfo( 333 | display_time=frame_state.predicted_display_time, 334 | environment_blend_mode=xr.EnvironmentBlendMode.OPAQUE, 335 | layers=render_layers, 336 | ) 337 | ) 338 | # xr.request_exit_session(session) # Request exit here allows clean exit but steamvr is hosed. 339 | if frame_count > 100: 340 | xr.request_exit_session(session) 341 | else: 342 | time.sleep(0.02) 343 | 344 | # Clean up 345 | graphics.destroy() 346 | logger.info("About to call xr.destroy_instance()...") 347 | xr.destroy_instance(instance) 348 | logger.info("... called xr.destroy_instance().") 349 | --------------------------------------------------------------------------------