├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── webxr-api ├── Cargo.toml ├── device.rs ├── error.rs ├── events.rs ├── frame.rs ├── hand.rs ├── hittest.rs ├── input.rs ├── layer.rs ├── lib.rs ├── mock.rs ├── registry.rs ├── session.rs ├── space.rs ├── util.rs └── view.rs └── webxr ├── Cargo.toml ├── gl_utils.rs ├── glwindow └── mod.rs ├── headless └── mod.rs ├── lib.rs ├── openxr ├── graphics.rs ├── graphics_d3d11.rs ├── input.rs ├── interaction_profiles.rs └── mod.rs └── surfman_layer_manager.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | merge_group: 8 | types: [checks_requested] 9 | 10 | env: 11 | SHELL: /bin/bash 12 | 13 | jobs: 14 | fmt: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: dtolnay/rust-toolchain@stable 19 | - name: fmt check 20 | run: cargo fmt --all -- --check 21 | linux: 22 | runs-on: ubuntu-22.04 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: dtolnay/rust-toolchain@stable 26 | - name: build 27 | run: | 28 | cd webxr 29 | cargo build --features=glwindow,headless 30 | cargo build --features=ipc,glwindow,headless 31 | cargo build --features=glwindow,headless 32 | mac: 33 | runs-on: macos-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: dtolnay/rust-toolchain@stable 37 | - name: build 38 | run: | 39 | cd webxr 40 | cargo build --features=glwindow,headless 41 | cargo build --features=ipc,glwindow,headless 42 | cargo build --features=glwindow,headless 43 | win: 44 | runs-on: windows-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: dtolnay/rust-toolchain@stable 48 | - name: build 49 | run: | 50 | cd webxr 51 | cargo build --features=glwindow,headless 52 | cargo build --features=ipc,glwindow,headless 53 | cargo build --features=glwindow,headless 54 | rustup target add aarch64-pc-windows-msvc 55 | cargo build --target=aarch64-pc-windows-msvc --features ipc,openxr-api 56 | build_result: 57 | name: Result 58 | runs-on: ubuntu-latest 59 | needs: ["mac", "linux", "win", "fmt"] 60 | steps: 61 | - name: Mark the job as successful 62 | run: exit 0 63 | if: success() 64 | - name: Mark the job as unsuccessful 65 | run: exit 1 66 | if: "!success()" 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | target 3 | Cargo.lock 4 | 5 | .idea 6 | .vscode 7 | .gradle/ 8 | build/ 9 | *.iml 10 | local.properties 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "webxr", 4 | "webxr-api" 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved in servo/[components/webxr](https://github.com/servo/servo/tree/5466c27f6f9a151ae7f5357cb663cc2580fbca15/components/webxr) with https://github.com/servo/servo/pull/35228 2 | 3 | A safe Rust API that provides a way to interact with virtual reality and 4 | augmented reality devices and integration with OpenXR. The API is inspired by 5 | the WebXR Device API (https://www.w3.org/TR/webxr/) but adapted to Rust design 6 | patterns. 7 | 8 | It's used in the WebXR implementation for the Servo browser, but can be used 9 | outside of Servo. 10 | 11 | Some notes on our plans for this API: 12 | https://paper.dropbox.com/doc/Rust-webxr-plans--Ad8iXHKI15DKsFhfT3ufGiQiAg-xmqpUtCzy8yNMGWwUUxsz 13 | -------------------------------------------------------------------------------- /webxr-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webxr-api" 3 | version = "0.0.1" 4 | authors = ["The Servo Project Developers"] 5 | edition = "2018" 6 | 7 | homepage = "https://github.com/servo/webxr" 8 | repository = "https://github.com/servo/webxr" 9 | keywords = ["ar", "headset", "openxr", "vr", "webxr"] 10 | license = "MPL-2.0" 11 | 12 | description = '''A safe Rust API that provides a way to interact with 13 | virtual reality and augmented reality devices and integration with OpenXR. 14 | The API is inspired by the WebXR Device API (https://www.w3.org/TR/webxr/) 15 | but adapted to Rust design patterns.''' 16 | 17 | [lib] 18 | path = "lib.rs" 19 | 20 | [features] 21 | ipc = ["serde", "ipc-channel", "euclid/serde"] 22 | 23 | [dependencies] 24 | euclid = "0.22" 25 | ipc-channel = { version = "0.19", optional = true } 26 | log = "0.4" 27 | serde = { version = "1.0", optional = true } 28 | time = { version = "0.1", optional = true } 29 | -------------------------------------------------------------------------------- /webxr-api/device.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! Traits to be implemented by backends 6 | 7 | use crate::ContextId; 8 | use crate::EnvironmentBlendMode; 9 | use crate::Error; 10 | use crate::Event; 11 | use crate::Floor; 12 | use crate::Frame; 13 | use crate::HitTestId; 14 | use crate::HitTestSource; 15 | use crate::InputSource; 16 | use crate::LayerId; 17 | use crate::LayerInit; 18 | use crate::Native; 19 | use crate::Quitter; 20 | use crate::Sender; 21 | use crate::Session; 22 | use crate::SessionBuilder; 23 | use crate::SessionInit; 24 | use crate::SessionMode; 25 | use crate::Viewports; 26 | 27 | use euclid::{Point2D, RigidTransform3D}; 28 | 29 | /// A trait for discovering XR devices 30 | pub trait DiscoveryAPI: 'static { 31 | fn request_session( 32 | &mut self, 33 | mode: SessionMode, 34 | init: &SessionInit, 35 | xr: SessionBuilder, 36 | ) -> Result; 37 | fn supports_session(&self, mode: SessionMode) -> bool; 38 | } 39 | 40 | /// A trait for using an XR device 41 | pub trait DeviceAPI: 'static { 42 | /// Create a new layer 43 | fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result; 44 | 45 | /// Destroy a layer 46 | fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId); 47 | 48 | /// The transform from native coordinates to the floor. 49 | fn floor_transform(&self) -> Option>; 50 | 51 | fn viewports(&self) -> Viewports; 52 | 53 | /// Begin an animation frame. 54 | fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option; 55 | 56 | /// End an animation frame, render the layer to the device, and block waiting for the next frame. 57 | fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]); 58 | 59 | /// Inputs registered with the device on initialization. More may be added, which 60 | /// should be communicated through a yet-undecided event mechanism 61 | fn initial_inputs(&self) -> Vec; 62 | 63 | /// Sets the event handling channel 64 | fn set_event_dest(&mut self, dest: Sender); 65 | 66 | /// Quit the session 67 | fn quit(&mut self); 68 | 69 | fn set_quitter(&mut self, quitter: Quitter); 70 | 71 | fn update_clip_planes(&mut self, near: f32, far: f32); 72 | 73 | fn environment_blend_mode(&self) -> EnvironmentBlendMode { 74 | // for VR devices, override for AR 75 | EnvironmentBlendMode::Opaque 76 | } 77 | 78 | fn granted_features(&self) -> &[String]; 79 | 80 | fn request_hit_test(&mut self, _source: HitTestSource) { 81 | panic!("This device does not support requesting hit tests"); 82 | } 83 | 84 | fn cancel_hit_test(&mut self, _id: HitTestId) { 85 | panic!("This device does not support hit tests"); 86 | } 87 | 88 | fn update_frame_rate(&mut self, rate: f32) -> f32 { 89 | rate 90 | } 91 | 92 | fn supported_frame_rates(&self) -> Vec { 93 | Vec::new() 94 | } 95 | 96 | fn reference_space_bounds(&self) -> Option>> { 97 | None 98 | } 99 | } 100 | 101 | impl DiscoveryAPI for Box> { 102 | fn request_session( 103 | &mut self, 104 | mode: SessionMode, 105 | init: &SessionInit, 106 | xr: SessionBuilder, 107 | ) -> Result { 108 | (&mut **self).request_session(mode, init, xr) 109 | } 110 | 111 | fn supports_session(&self, mode: SessionMode) -> bool { 112 | (&**self).supports_session(mode) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /webxr-api/error.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #[cfg(feature = "ipc")] 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Errors that can be produced by XR. 9 | 10 | // TODO: this is currently incomplete! 11 | 12 | #[derive(Debug)] 13 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 14 | pub enum Error { 15 | NoMatchingDevice, 16 | CommunicationError, 17 | ThreadCreationError, 18 | InlineSession, 19 | UnsupportedFeature(String), 20 | BackendSpecific(String), 21 | } 22 | -------------------------------------------------------------------------------- /webxr-api/events.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use euclid::RigidTransform3D; 6 | 7 | use crate::ApiSpace; 8 | use crate::BaseSpace; 9 | use crate::Frame; 10 | use crate::InputFrame; 11 | use crate::InputId; 12 | use crate::InputSource; 13 | use crate::SelectEvent; 14 | use crate::SelectKind; 15 | use crate::Sender; 16 | 17 | #[derive(Clone, Debug)] 18 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 19 | pub enum Event { 20 | /// Input source connected 21 | AddInput(InputSource), 22 | /// Input source disconnected 23 | RemoveInput(InputId), 24 | /// Input updated (this is a disconnect+reconnect) 25 | UpdateInput(InputId, InputSource), 26 | /// Session ended by device 27 | SessionEnd, 28 | /// Session focused/blurred/etc 29 | VisibilityChange(Visibility), 30 | /// Selection started / ended 31 | Select(InputId, SelectKind, SelectEvent, Frame), 32 | /// Input from an input source has changed 33 | InputChanged(InputId, InputFrame), 34 | /// Reference space has changed 35 | ReferenceSpaceChanged(BaseSpace, RigidTransform3D), 36 | } 37 | 38 | #[derive(Copy, Clone, Debug)] 39 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 40 | pub enum Visibility { 41 | /// Session fully displayed to user 42 | Visible, 43 | /// Session still visible, but is not the primary focus 44 | VisibleBlurred, 45 | /// Session not visible 46 | Hidden, 47 | } 48 | 49 | /// Convenience structure for buffering up events 50 | /// when no event callback has been set 51 | pub enum EventBuffer { 52 | Buffered(Vec), 53 | Sink(Sender), 54 | } 55 | 56 | impl Default for EventBuffer { 57 | fn default() -> Self { 58 | EventBuffer::Buffered(vec![]) 59 | } 60 | } 61 | 62 | impl EventBuffer { 63 | pub fn callback(&mut self, event: Event) { 64 | match *self { 65 | EventBuffer::Buffered(ref mut events) => events.push(event), 66 | EventBuffer::Sink(ref dest) => { 67 | let _ = dest.send(event); 68 | } 69 | } 70 | } 71 | 72 | pub fn upgrade(&mut self, dest: Sender) { 73 | if let EventBuffer::Buffered(ref mut events) = *self { 74 | for event in events.drain(..) { 75 | let _ = dest.send(event); 76 | } 77 | } 78 | *self = EventBuffer::Sink(dest) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /webxr-api/frame.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::Floor; 6 | use crate::HitTestId; 7 | use crate::HitTestResult; 8 | use crate::InputFrame; 9 | use crate::Native; 10 | use crate::SubImages; 11 | use crate::Viewer; 12 | use crate::Viewports; 13 | use crate::Views; 14 | 15 | use euclid::RigidTransform3D; 16 | 17 | /// The per-frame data that is provided by the device. 18 | /// https://www.w3.org/TR/webxr/#xrframe 19 | // TODO: other fields? 20 | #[derive(Clone, Debug)] 21 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 22 | pub struct Frame { 23 | /// The pose information of the viewer 24 | pub pose: Option, 25 | /// Frame information for each connected input source 26 | pub inputs: Vec, 27 | 28 | /// Events that occur with the frame. 29 | pub events: Vec, 30 | 31 | /// The subimages to render to 32 | pub sub_images: Vec, 33 | 34 | /// The hit test results for this frame, if any 35 | pub hit_test_results: Vec, 36 | 37 | /// The average point in time this XRFrame is expected to be displayed on the devices' display 38 | pub predicted_display_time: f64, 39 | } 40 | 41 | #[derive(Clone, Debug)] 42 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 43 | pub enum FrameUpdateEvent { 44 | UpdateFloorTransform(Option>), 45 | UpdateViewports(Viewports), 46 | HitTestSourceAdded(HitTestId), 47 | } 48 | 49 | #[derive(Clone, Debug)] 50 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 51 | pub struct ViewerPose { 52 | /// The transform from the viewer to native coordinates 53 | /// 54 | /// This is equivalent to the pose of the viewer in native coordinates. 55 | /// This is the inverse of the view matrix. 56 | pub transform: RigidTransform3D, 57 | 58 | // The various views 59 | pub views: Views, 60 | } 61 | -------------------------------------------------------------------------------- /webxr-api/hand.rs: -------------------------------------------------------------------------------- 1 | use crate::Native; 2 | use euclid::RigidTransform3D; 3 | 4 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 5 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 6 | pub struct HandSpace; 7 | 8 | #[derive(Clone, Debug, Default)] 9 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 10 | pub struct Hand { 11 | pub wrist: Option, 12 | pub thumb_metacarpal: Option, 13 | pub thumb_phalanx_proximal: Option, 14 | pub thumb_phalanx_distal: Option, 15 | pub thumb_phalanx_tip: Option, 16 | pub index: Finger, 17 | pub middle: Finger, 18 | pub ring: Finger, 19 | pub little: Finger, 20 | } 21 | 22 | #[derive(Clone, Debug, Default)] 23 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 24 | pub struct Finger { 25 | pub metacarpal: Option, 26 | pub phalanx_proximal: Option, 27 | pub phalanx_intermediate: Option, 28 | pub phalanx_distal: Option, 29 | pub phalanx_tip: Option, 30 | } 31 | 32 | #[derive(Copy, Clone, Debug)] 33 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 34 | pub struct JointFrame { 35 | pub pose: RigidTransform3D, 36 | pub radius: f32, 37 | } 38 | 39 | impl Default for JointFrame { 40 | fn default() -> Self { 41 | Self { 42 | pose: RigidTransform3D::identity(), 43 | radius: 0., 44 | } 45 | } 46 | } 47 | 48 | impl Hand { 49 | pub fn map(&self, map: impl (Fn(&Option, Joint) -> Option) + Copy) -> Hand { 50 | Hand { 51 | wrist: map(&self.wrist, Joint::Wrist), 52 | thumb_metacarpal: map(&self.thumb_metacarpal, Joint::ThumbMetacarpal), 53 | thumb_phalanx_proximal: map(&self.thumb_phalanx_proximal, Joint::ThumbPhalanxProximal), 54 | thumb_phalanx_distal: map(&self.thumb_phalanx_distal, Joint::ThumbPhalanxDistal), 55 | thumb_phalanx_tip: map(&self.thumb_phalanx_tip, Joint::ThumbPhalanxTip), 56 | index: self.index.map(|f, j| map(f, Joint::Index(j))), 57 | middle: self.middle.map(|f, j| map(f, Joint::Middle(j))), 58 | ring: self.ring.map(|f, j| map(f, Joint::Ring(j))), 59 | little: self.little.map(|f, j| map(f, Joint::Little(j))), 60 | } 61 | } 62 | 63 | pub fn get(&self, joint: Joint) -> Option<&J> { 64 | match joint { 65 | Joint::Wrist => self.wrist.as_ref(), 66 | Joint::ThumbMetacarpal => self.thumb_metacarpal.as_ref(), 67 | Joint::ThumbPhalanxProximal => self.thumb_phalanx_proximal.as_ref(), 68 | Joint::ThumbPhalanxDistal => self.thumb_phalanx_distal.as_ref(), 69 | Joint::ThumbPhalanxTip => self.thumb_phalanx_tip.as_ref(), 70 | Joint::Index(f) => self.index.get(f), 71 | Joint::Middle(f) => self.middle.get(f), 72 | Joint::Ring(f) => self.ring.get(f), 73 | Joint::Little(f) => self.little.get(f), 74 | } 75 | } 76 | } 77 | 78 | impl Finger { 79 | pub fn map(&self, map: impl (Fn(&Option, FingerJoint) -> Option) + Copy) -> Finger { 80 | Finger { 81 | metacarpal: map(&self.metacarpal, FingerJoint::Metacarpal), 82 | phalanx_proximal: map(&self.phalanx_proximal, FingerJoint::PhalanxProximal), 83 | phalanx_intermediate: map(&self.phalanx_intermediate, FingerJoint::PhalanxIntermediate), 84 | phalanx_distal: map(&self.phalanx_distal, FingerJoint::PhalanxDistal), 85 | phalanx_tip: map(&self.phalanx_tip, FingerJoint::PhalanxTip), 86 | } 87 | } 88 | 89 | pub fn get(&self, joint: FingerJoint) -> Option<&J> { 90 | match joint { 91 | FingerJoint::Metacarpal => self.metacarpal.as_ref(), 92 | FingerJoint::PhalanxProximal => self.phalanx_proximal.as_ref(), 93 | FingerJoint::PhalanxIntermediate => self.phalanx_intermediate.as_ref(), 94 | FingerJoint::PhalanxDistal => self.phalanx_distal.as_ref(), 95 | FingerJoint::PhalanxTip => self.phalanx_tip.as_ref(), 96 | } 97 | } 98 | } 99 | 100 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 101 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 102 | pub enum FingerJoint { 103 | Metacarpal, 104 | PhalanxProximal, 105 | PhalanxIntermediate, 106 | PhalanxDistal, 107 | PhalanxTip, 108 | } 109 | 110 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 111 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 112 | pub enum Joint { 113 | Wrist, 114 | ThumbMetacarpal, 115 | ThumbPhalanxProximal, 116 | ThumbPhalanxDistal, 117 | ThumbPhalanxTip, 118 | Index(FingerJoint), 119 | Middle(FingerJoint), 120 | Ring(FingerJoint), 121 | Little(FingerJoint), 122 | } 123 | -------------------------------------------------------------------------------- /webxr-api/hittest.rs: -------------------------------------------------------------------------------- 1 | use crate::ApiSpace; 2 | use crate::Native; 3 | use crate::Space; 4 | use euclid::Point3D; 5 | use euclid::RigidTransform3D; 6 | use euclid::Rotation3D; 7 | use euclid::Vector3D; 8 | use std::f32::EPSILON; 9 | use std::iter::FromIterator; 10 | 11 | #[derive(Clone, Copy, Debug)] 12 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 13 | /// https://immersive-web.github.io/hit-test/#xrray 14 | pub struct Ray { 15 | /// The origin of the ray 16 | pub origin: Vector3D, 17 | /// The direction of the ray. Must be normalized. 18 | pub direction: Vector3D, 19 | } 20 | 21 | #[derive(Clone, Copy, Debug)] 22 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 23 | /// https://immersive-web.github.io/hit-test/#enumdef-xrhittesttrackabletype 24 | pub enum EntityType { 25 | Point, 26 | Plane, 27 | Mesh, 28 | } 29 | 30 | #[derive(Copy, Clone, Debug)] 31 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 32 | /// https://immersive-web.github.io/hit-test/#dictdef-xrhittestoptionsinit 33 | pub struct HitTestSource { 34 | pub id: HitTestId, 35 | pub space: Space, 36 | pub ray: Ray, 37 | pub types: EntityTypes, 38 | } 39 | 40 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 41 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 42 | pub struct HitTestId(pub u32); 43 | 44 | #[derive(Copy, Clone, Debug, Default)] 45 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 46 | /// Vec, but better 47 | pub struct EntityTypes { 48 | pub point: bool, 49 | pub plane: bool, 50 | pub mesh: bool, 51 | } 52 | 53 | #[derive(Copy, Clone, Debug)] 54 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 55 | pub struct HitTestResult { 56 | pub id: HitTestId, 57 | pub space: RigidTransform3D, 58 | } 59 | 60 | #[derive(Clone, Copy, Debug)] 61 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 62 | /// The coordinate space of a hit test result 63 | pub struct HitTestSpace; 64 | 65 | #[derive(Copy, Clone, Debug)] 66 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 67 | pub struct Triangle { 68 | pub first: Point3D, 69 | pub second: Point3D, 70 | pub third: Point3D, 71 | } 72 | 73 | impl EntityTypes { 74 | pub fn is_type(self, ty: EntityType) -> bool { 75 | match ty { 76 | EntityType::Point => self.point, 77 | EntityType::Plane => self.plane, 78 | EntityType::Mesh => self.mesh, 79 | } 80 | } 81 | 82 | pub fn add_type(&mut self, ty: EntityType) { 83 | match ty { 84 | EntityType::Point => self.point = true, 85 | EntityType::Plane => self.plane = true, 86 | EntityType::Mesh => self.mesh = true, 87 | } 88 | } 89 | } 90 | 91 | impl FromIterator for EntityTypes { 92 | fn from_iter(iter: T) -> Self 93 | where 94 | T: IntoIterator, 95 | { 96 | iter.into_iter().fold(Default::default(), |mut acc, e| { 97 | acc.add_type(e); 98 | acc 99 | }) 100 | } 101 | } 102 | 103 | impl Triangle { 104 | /// https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm 105 | pub fn intersect( 106 | self, 107 | ray: Ray, 108 | ) -> Option> { 109 | let Triangle { 110 | first: v0, 111 | second: v1, 112 | third: v2, 113 | } = self; 114 | 115 | let edge1 = v1 - v0; 116 | let edge2 = v2 - v0; 117 | 118 | let h = ray.direction.cross(edge2); 119 | let a = edge1.dot(h); 120 | if a > -EPSILON && a < EPSILON { 121 | // ray is parallel to triangle 122 | return None; 123 | } 124 | 125 | let f = 1. / a; 126 | 127 | let s = ray.origin - v0.to_vector(); 128 | 129 | // barycentric coordinate of intersection point u 130 | let u = f * s.dot(h); 131 | // barycentric coordinates have range (0, 1) 132 | if u < 0. || u > 1. { 133 | // the intersection is outside the triangle 134 | return None; 135 | } 136 | 137 | let q = s.cross(edge1); 138 | // barycentric coordinate of intersection point v 139 | let v = f * ray.direction.dot(q); 140 | 141 | // barycentric coordinates have range (0, 1) 142 | // and their sum must not be greater than 1 143 | if v < 0. || u + v > 1. { 144 | // the intersection is outside the triangle 145 | return None; 146 | } 147 | 148 | let t = f * edge2.dot(q); 149 | 150 | if t > EPSILON { 151 | let origin = ray.origin + ray.direction * t; 152 | 153 | // this is not part of the Möller-Trumbore algorithm, the hit test spec 154 | // requires it has an orientation such that the Y axis points along 155 | // the triangle normal 156 | let normal = edge1.cross(edge2).normalize(); 157 | let y = Vector3D::new(0., 1., 0.); 158 | let dot = normal.dot(y); 159 | let rotation = if dot > -EPSILON && dot < EPSILON { 160 | // vectors are parallel, return the vector itself 161 | // XXXManishearth it's possible for the vectors to be 162 | // antiparallel, unclear if normals need to be flipped 163 | Rotation3D::identity() 164 | } else { 165 | let axis = normal.cross(y); 166 | let cos = normal.dot(y); 167 | // This is Rotation3D::around_axis(axis.normalize(), theta), however 168 | // that is just Rotation3D::quaternion(axis.normalize().xyz * sin, cos), 169 | // which is Rotation3D::quaternion(cross, dot) 170 | Rotation3D::quaternion(axis.x, axis.y, axis.z, cos) 171 | }; 172 | 173 | return Some(RigidTransform3D::new(rotation, origin)); 174 | } 175 | 176 | // triangle is behind ray 177 | None 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /webxr-api/input.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::Hand; 6 | use crate::Input; 7 | use crate::JointFrame; 8 | use crate::Native; 9 | 10 | use euclid::RigidTransform3D; 11 | 12 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 13 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 14 | pub struct InputId(pub u32); 15 | 16 | #[derive(Copy, Clone, Debug)] 17 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 18 | pub enum Handedness { 19 | None, 20 | Left, 21 | Right, 22 | } 23 | 24 | #[derive(Copy, Clone, Debug)] 25 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 26 | pub enum TargetRayMode { 27 | Gaze, 28 | TrackedPointer, 29 | Screen, 30 | TransientPointer, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 35 | pub struct InputSource { 36 | pub handedness: Handedness, 37 | pub target_ray_mode: TargetRayMode, 38 | pub id: InputId, 39 | pub supports_grip: bool, 40 | pub hand_support: Option>, 41 | pub profiles: Vec, 42 | } 43 | 44 | #[derive(Clone, Debug)] 45 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 46 | pub struct InputFrame { 47 | pub id: InputId, 48 | pub target_ray_origin: Option>, 49 | pub grip_origin: Option>, 50 | pub pressed: bool, 51 | pub hand: Option>>, 52 | pub squeezed: bool, 53 | pub button_values: Vec, 54 | pub axis_values: Vec, 55 | pub input_changed: bool, 56 | } 57 | 58 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 59 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 60 | pub enum SelectEvent { 61 | /// Selection started 62 | Start, 63 | /// Selection ended *without* it being a contiguous select event 64 | End, 65 | /// Selection ended *with* it being a contiguous select event 66 | Select, 67 | } 68 | 69 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 70 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 71 | pub enum SelectKind { 72 | Select, 73 | Squeeze, 74 | } 75 | -------------------------------------------------------------------------------- /webxr-api/layer.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::Error; 6 | use crate::Viewport; 7 | use crate::Viewports; 8 | 9 | use euclid::Rect; 10 | use euclid::Size2D; 11 | 12 | use std::fmt::Debug; 13 | use std::sync::atomic::AtomicUsize; 14 | use std::sync::atomic::Ordering; 15 | 16 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 17 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 18 | pub struct ContextId(pub u64); 19 | 20 | #[cfg(feature = "ipc")] 21 | use serde::{Deserialize, Serialize}; 22 | 23 | pub trait GLTypes { 24 | type Device; 25 | type Context; 26 | type Bindings; 27 | } 28 | 29 | pub trait GLContexts { 30 | fn bindings(&mut self, device: &GL::Device, context_id: ContextId) -> Option<&GL::Bindings>; 31 | fn context(&mut self, device: &GL::Device, context_id: ContextId) -> Option<&mut GL::Context>; 32 | } 33 | 34 | impl GLTypes for () { 35 | type Bindings = (); 36 | type Device = (); 37 | type Context = (); 38 | } 39 | 40 | impl GLContexts<()> for () { 41 | fn context(&mut self, _: &(), _: ContextId) -> Option<&mut ()> { 42 | Some(self) 43 | } 44 | 45 | fn bindings(&mut self, _: &(), _: ContextId) -> Option<&()> { 46 | Some(self) 47 | } 48 | } 49 | 50 | pub trait LayerGrandManagerAPI { 51 | fn create_layer_manager(&self, factory: LayerManagerFactory) 52 | -> Result; 53 | 54 | fn clone_layer_grand_manager(&self) -> LayerGrandManager; 55 | } 56 | 57 | pub struct LayerGrandManager(Box>); 58 | 59 | impl Clone for LayerGrandManager { 60 | fn clone(&self) -> Self { 61 | self.0.clone_layer_grand_manager() 62 | } 63 | } 64 | 65 | impl Debug for LayerGrandManager { 66 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 67 | "LayerGrandManager(...)".fmt(fmt) 68 | } 69 | } 70 | 71 | impl LayerGrandManager { 72 | pub fn new(grand_manager: GM) -> LayerGrandManager 73 | where 74 | GM: 'static + Send + LayerGrandManagerAPI, 75 | { 76 | LayerGrandManager(Box::new(grand_manager)) 77 | } 78 | 79 | pub fn create_layer_manager(&self, factory: F) -> Result 80 | where 81 | F: 'static + Send + FnOnce(&mut GL::Device, &mut dyn GLContexts) -> Result, 82 | M: 'static + LayerManagerAPI, 83 | { 84 | self.0 85 | .create_layer_manager(LayerManagerFactory::new(factory)) 86 | } 87 | } 88 | 89 | pub trait LayerManagerAPI { 90 | fn create_layer( 91 | &mut self, 92 | device: &mut GL::Device, 93 | contexts: &mut dyn GLContexts, 94 | context_id: ContextId, 95 | init: LayerInit, 96 | ) -> Result; 97 | 98 | fn destroy_layer( 99 | &mut self, 100 | device: &mut GL::Device, 101 | contexts: &mut dyn GLContexts, 102 | context_id: ContextId, 103 | layer_id: LayerId, 104 | ); 105 | 106 | fn layers(&self) -> &[(ContextId, LayerId)]; 107 | 108 | fn begin_frame( 109 | &mut self, 110 | device: &mut GL::Device, 111 | contexts: &mut dyn GLContexts, 112 | layers: &[(ContextId, LayerId)], 113 | ) -> Result, Error>; 114 | 115 | fn end_frame( 116 | &mut self, 117 | device: &mut GL::Device, 118 | contexts: &mut dyn GLContexts, 119 | layers: &[(ContextId, LayerId)], 120 | ) -> Result<(), Error>; 121 | } 122 | 123 | pub struct LayerManager(Box>); 124 | 125 | impl Debug for LayerManager { 126 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 127 | "LayerManager(...)".fmt(fmt) 128 | } 129 | } 130 | 131 | impl LayerManager { 132 | pub fn create_layer( 133 | &mut self, 134 | context_id: ContextId, 135 | init: LayerInit, 136 | ) -> Result { 137 | self.0.create_layer(&mut (), &mut (), context_id, init) 138 | } 139 | 140 | pub fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) { 141 | self.0.destroy_layer(&mut (), &mut (), context_id, layer_id); 142 | } 143 | 144 | pub fn begin_frame( 145 | &mut self, 146 | layers: &[(ContextId, LayerId)], 147 | ) -> Result, Error> { 148 | self.0.begin_frame(&mut (), &mut (), layers) 149 | } 150 | 151 | pub fn end_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Result<(), Error> { 152 | self.0.end_frame(&mut (), &mut (), layers) 153 | } 154 | } 155 | 156 | impl LayerManager { 157 | pub fn new(manager: M) -> LayerManager 158 | where 159 | M: 'static + Send + LayerManagerAPI<()>, 160 | { 161 | LayerManager(Box::new(manager)) 162 | } 163 | } 164 | 165 | impl Drop for LayerManager { 166 | fn drop(&mut self) { 167 | log::debug!("Dropping LayerManager"); 168 | for (context_id, layer_id) in self.0.layers().to_vec() { 169 | self.destroy_layer(context_id, layer_id); 170 | } 171 | } 172 | } 173 | 174 | pub struct LayerManagerFactory( 175 | Box< 176 | dyn Send 177 | + FnOnce( 178 | &mut GL::Device, 179 | &mut dyn GLContexts, 180 | ) -> Result>, Error>, 181 | >, 182 | ); 183 | 184 | impl Debug for LayerManagerFactory { 185 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 186 | "LayerManagerFactory(...)".fmt(fmt) 187 | } 188 | } 189 | 190 | impl LayerManagerFactory { 191 | pub fn new(factory: F) -> LayerManagerFactory 192 | where 193 | F: 'static + Send + FnOnce(&mut GL::Device, &mut dyn GLContexts) -> Result, 194 | M: 'static + LayerManagerAPI, 195 | { 196 | LayerManagerFactory(Box::new(move |device, contexts| { 197 | Ok(Box::new(factory(device, contexts)?)) 198 | })) 199 | } 200 | 201 | pub fn build( 202 | self, 203 | device: &mut GL::Device, 204 | contexts: &mut dyn GLContexts, 205 | ) -> Result>, Error> { 206 | (self.0)(device, contexts) 207 | } 208 | } 209 | 210 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 211 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 212 | pub struct LayerId(usize); 213 | 214 | static NEXT_LAYER_ID: AtomicUsize = AtomicUsize::new(0); 215 | 216 | impl LayerId { 217 | pub fn new() -> LayerId { 218 | LayerId(NEXT_LAYER_ID.fetch_add(1, Ordering::SeqCst)) 219 | } 220 | } 221 | 222 | #[derive(Copy, Clone, Debug)] 223 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 224 | pub enum LayerInit { 225 | // https://www.w3.org/TR/webxr/#dictdef-xrwebgllayerinit 226 | WebGLLayer { 227 | antialias: bool, 228 | depth: bool, 229 | stencil: bool, 230 | alpha: bool, 231 | ignore_depth_values: bool, 232 | framebuffer_scale_factor: f32, 233 | }, 234 | // https://immersive-web.github.io/layers/#xrprojectionlayerinittype 235 | ProjectionLayer { 236 | depth: bool, 237 | stencil: bool, 238 | alpha: bool, 239 | scale_factor: f32, 240 | }, 241 | // TODO: other layer types 242 | } 243 | 244 | impl LayerInit { 245 | pub fn texture_size(&self, viewports: &Viewports) -> Size2D { 246 | match self { 247 | LayerInit::WebGLLayer { 248 | framebuffer_scale_factor: scale, 249 | .. 250 | } 251 | | LayerInit::ProjectionLayer { 252 | scale_factor: scale, 253 | .. 254 | } => { 255 | let native_size = viewports 256 | .viewports 257 | .iter() 258 | .fold(Rect::zero(), |acc, view| acc.union(view)) 259 | .size; 260 | (native_size.to_f32() * *scale).to_i32() 261 | } 262 | } 263 | } 264 | } 265 | 266 | /// https://immersive-web.github.io/layers/#enumdef-xrlayerlayout 267 | #[derive(Copy, Clone, Debug)] 268 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 269 | pub enum LayerLayout { 270 | // TODO: Default 271 | // Allocates one texture 272 | Mono, 273 | // Allocates one texture, which is split in half vertically, giving two subimages 274 | StereoLeftRight, 275 | // Allocates one texture, which is split in half horizonally, giving two subimages 276 | StereoTopBottom, 277 | } 278 | 279 | #[derive(Clone, Debug)] 280 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 281 | pub struct SubImages { 282 | pub layer_id: LayerId, 283 | pub sub_image: Option, 284 | pub view_sub_images: Vec, 285 | } 286 | 287 | /// https://immersive-web.github.io/layers/#xrsubimagetype 288 | #[derive(Clone, Debug)] 289 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 290 | pub struct SubImage { 291 | pub color_texture: u32, 292 | // TODO: make this Option 293 | pub depth_stencil_texture: Option, 294 | pub texture_array_index: Option, 295 | pub viewport: Rect, 296 | } 297 | -------------------------------------------------------------------------------- /webxr-api/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This crate defines the Rust API for WebXR. It is implemented by the `webxr` crate. 6 | 7 | mod device; 8 | mod error; 9 | mod events; 10 | mod frame; 11 | mod hand; 12 | mod hittest; 13 | mod input; 14 | mod layer; 15 | mod mock; 16 | mod registry; 17 | mod session; 18 | mod space; 19 | pub mod util; 20 | mod view; 21 | 22 | pub use device::DeviceAPI; 23 | pub use device::DiscoveryAPI; 24 | 25 | pub use error::Error; 26 | 27 | pub use events::Event; 28 | pub use events::EventBuffer; 29 | pub use events::Visibility; 30 | 31 | pub use frame::Frame; 32 | pub use frame::FrameUpdateEvent; 33 | pub use frame::ViewerPose; 34 | 35 | pub use hand::Finger; 36 | pub use hand::FingerJoint; 37 | pub use hand::Hand; 38 | pub use hand::HandSpace; 39 | pub use hand::Joint; 40 | pub use hand::JointFrame; 41 | 42 | pub use hittest::EntityType; 43 | pub use hittest::EntityTypes; 44 | pub use hittest::HitTestId; 45 | pub use hittest::HitTestResult; 46 | pub use hittest::HitTestSource; 47 | pub use hittest::HitTestSpace; 48 | pub use hittest::Ray; 49 | pub use hittest::Triangle; 50 | 51 | pub use input::Handedness; 52 | pub use input::InputFrame; 53 | pub use input::InputId; 54 | pub use input::InputSource; 55 | pub use input::SelectEvent; 56 | pub use input::SelectKind; 57 | pub use input::TargetRayMode; 58 | 59 | pub use layer::ContextId; 60 | pub use layer::GLContexts; 61 | pub use layer::GLTypes; 62 | pub use layer::LayerGrandManager; 63 | pub use layer::LayerGrandManagerAPI; 64 | pub use layer::LayerId; 65 | pub use layer::LayerInit; 66 | pub use layer::LayerLayout; 67 | pub use layer::LayerManager; 68 | pub use layer::LayerManagerAPI; 69 | pub use layer::LayerManagerFactory; 70 | pub use layer::SubImage; 71 | pub use layer::SubImages; 72 | 73 | pub use mock::MockButton; 74 | pub use mock::MockButtonType; 75 | pub use mock::MockDeviceInit; 76 | pub use mock::MockDeviceMsg; 77 | pub use mock::MockDiscoveryAPI; 78 | pub use mock::MockInputInit; 79 | pub use mock::MockInputMsg; 80 | pub use mock::MockRegion; 81 | pub use mock::MockViewInit; 82 | pub use mock::MockViewsInit; 83 | pub use mock::MockWorld; 84 | 85 | pub use registry::MainThreadRegistry; 86 | pub use registry::MainThreadWaker; 87 | pub use registry::Registry; 88 | 89 | pub use session::EnvironmentBlendMode; 90 | pub use session::MainThreadSession; 91 | pub use session::Quitter; 92 | pub use session::Session; 93 | pub use session::SessionBuilder; 94 | pub use session::SessionId; 95 | pub use session::SessionInit; 96 | pub use session::SessionMode; 97 | pub use session::SessionThread; 98 | 99 | pub use space::ApiSpace; 100 | pub use space::BaseSpace; 101 | pub use space::Space; 102 | 103 | pub use view::Capture; 104 | pub use view::CubeBack; 105 | pub use view::CubeBottom; 106 | pub use view::CubeLeft; 107 | pub use view::CubeRight; 108 | pub use view::CubeTop; 109 | pub use view::Display; 110 | pub use view::Floor; 111 | pub use view::Input; 112 | pub use view::LeftEye; 113 | pub use view::Native; 114 | pub use view::RightEye; 115 | pub use view::SomeEye; 116 | pub use view::View; 117 | pub use view::Viewer; 118 | pub use view::Viewport; 119 | pub use view::Viewports; 120 | pub use view::Views; 121 | pub use view::CUBE_BACK; 122 | pub use view::CUBE_BOTTOM; 123 | pub use view::CUBE_LEFT; 124 | pub use view::CUBE_RIGHT; 125 | pub use view::CUBE_TOP; 126 | pub use view::LEFT_EYE; 127 | pub use view::RIGHT_EYE; 128 | pub use view::VIEWER; 129 | 130 | #[cfg(feature = "ipc")] 131 | use std::thread; 132 | 133 | use std::time::Duration; 134 | 135 | #[cfg(feature = "ipc")] 136 | pub use ipc_channel::ipc::IpcSender as Sender; 137 | 138 | #[cfg(feature = "ipc")] 139 | pub use ipc_channel::ipc::IpcReceiver as Receiver; 140 | 141 | #[cfg(feature = "ipc")] 142 | pub use ipc_channel::ipc::channel; 143 | 144 | #[cfg(not(feature = "ipc"))] 145 | pub use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender}; 146 | 147 | #[cfg(not(feature = "ipc"))] 148 | pub fn channel() -> Result<(Sender, Receiver), ()> { 149 | Ok(std::sync::mpsc::channel()) 150 | } 151 | 152 | #[cfg(not(feature = "ipc"))] 153 | pub fn recv_timeout(receiver: &Receiver, timeout: Duration) -> Result { 154 | receiver.recv_timeout(timeout) 155 | } 156 | 157 | #[cfg(feature = "ipc")] 158 | pub fn recv_timeout( 159 | receiver: &Receiver, 160 | timeout: Duration, 161 | ) -> Result 162 | where 163 | T: serde::Serialize + for<'a> serde::Deserialize<'a>, 164 | { 165 | // Sigh, polling, sigh. 166 | let mut delay = timeout / 1000; 167 | while delay < timeout { 168 | if let Ok(msg) = receiver.try_recv() { 169 | return Ok(msg); 170 | } 171 | thread::sleep(delay); 172 | delay = delay * 2; 173 | } 174 | receiver.try_recv() 175 | } 176 | -------------------------------------------------------------------------------- /webxr-api/mock.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::DiscoveryAPI; 6 | use crate::Display; 7 | use crate::EntityType; 8 | use crate::Error; 9 | use crate::Floor; 10 | use crate::Handedness; 11 | use crate::Input; 12 | use crate::InputId; 13 | use crate::InputSource; 14 | use crate::LeftEye; 15 | use crate::Native; 16 | use crate::Receiver; 17 | use crate::RightEye; 18 | use crate::SelectEvent; 19 | use crate::SelectKind; 20 | use crate::Sender; 21 | use crate::TargetRayMode; 22 | use crate::Triangle; 23 | use crate::Viewer; 24 | use crate::Viewport; 25 | use crate::Visibility; 26 | 27 | use euclid::{Point2D, Rect, RigidTransform3D, Transform3D}; 28 | 29 | #[cfg(feature = "ipc")] 30 | use serde::{Deserialize, Serialize}; 31 | 32 | /// A trait for discovering mock XR devices 33 | pub trait MockDiscoveryAPI: 'static { 34 | fn simulate_device_connection( 35 | &mut self, 36 | init: MockDeviceInit, 37 | receiver: Receiver, 38 | ) -> Result>, Error>; 39 | } 40 | 41 | #[derive(Clone, Debug)] 42 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 43 | pub struct MockDeviceInit { 44 | pub floor_origin: Option>, 45 | pub supports_inline: bool, 46 | pub supports_vr: bool, 47 | pub supports_ar: bool, 48 | pub viewer_origin: Option>, 49 | pub views: MockViewsInit, 50 | pub supported_features: Vec, 51 | pub world: Option, 52 | } 53 | 54 | #[derive(Clone, Debug)] 55 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 56 | pub struct MockViewInit { 57 | pub transform: RigidTransform3D, 58 | pub projection: Transform3D, 59 | pub viewport: Rect, 60 | /// field of view values, in radians 61 | pub fov: Option<(f32, f32, f32, f32)>, 62 | } 63 | 64 | #[derive(Clone, Debug)] 65 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 66 | pub enum MockViewsInit { 67 | Mono(MockViewInit), 68 | Stereo(MockViewInit, MockViewInit), 69 | } 70 | 71 | #[derive(Debug)] 72 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 73 | pub enum MockDeviceMsg { 74 | SetViewerOrigin(Option>), 75 | SetFloorOrigin(Option>), 76 | SetViews(MockViewsInit), 77 | AddInputSource(MockInputInit), 78 | MessageInputSource(InputId, MockInputMsg), 79 | VisibilityChange(Visibility), 80 | SetWorld(MockWorld), 81 | ClearWorld, 82 | Disconnect(Sender<()>), 83 | SetBoundsGeometry(Vec>), 84 | SimulateResetPose, 85 | } 86 | 87 | #[derive(Clone, Debug)] 88 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 89 | pub struct MockInputInit { 90 | pub source: InputSource, 91 | pub pointer_origin: Option>, 92 | pub grip_origin: Option>, 93 | pub supported_buttons: Vec, 94 | } 95 | 96 | #[derive(Debug)] 97 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 98 | pub enum MockInputMsg { 99 | SetHandedness(Handedness), 100 | SetTargetRayMode(TargetRayMode), 101 | SetProfiles(Vec), 102 | SetPointerOrigin(Option>), 103 | SetGripOrigin(Option>), 104 | /// Note: SelectEvent::Select here refers to a complete Select event, 105 | /// not just the end event, i.e. it refers to 106 | /// https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-simulateselect 107 | TriggerSelect(SelectKind, SelectEvent), 108 | Disconnect, 109 | Reconnect, 110 | SetSupportedButtons(Vec), 111 | UpdateButtonState(MockButton), 112 | } 113 | 114 | #[derive(Clone, Debug)] 115 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 116 | pub struct MockRegion { 117 | pub faces: Vec, 118 | pub ty: EntityType, 119 | } 120 | 121 | #[derive(Clone, Debug)] 122 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 123 | pub struct MockWorld { 124 | pub regions: Vec, 125 | } 126 | 127 | #[derive(Clone, Debug, PartialEq)] 128 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 129 | pub enum MockButtonType { 130 | Grip, 131 | Touchpad, 132 | Thumbstick, 133 | OptionalButton, 134 | OptionalThumbstick, 135 | } 136 | 137 | #[derive(Clone, Debug)] 138 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 139 | pub struct MockButton { 140 | pub button_type: MockButtonType, 141 | pub pressed: bool, 142 | pub touched: bool, 143 | pub pressed_value: f32, 144 | pub x_value: f32, 145 | pub y_value: f32, 146 | } 147 | -------------------------------------------------------------------------------- /webxr-api/registry.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::DiscoveryAPI; 6 | use crate::Error; 7 | use crate::Frame; 8 | use crate::GLTypes; 9 | use crate::LayerGrandManager; 10 | use crate::MainThreadSession; 11 | use crate::MockDeviceInit; 12 | use crate::MockDeviceMsg; 13 | use crate::MockDiscoveryAPI; 14 | use crate::Receiver; 15 | use crate::Sender; 16 | use crate::Session; 17 | use crate::SessionBuilder; 18 | use crate::SessionId; 19 | use crate::SessionInit; 20 | use crate::SessionMode; 21 | 22 | use log::warn; 23 | 24 | #[cfg(feature = "ipc")] 25 | use serde::{Deserialize, Serialize}; 26 | 27 | #[derive(Clone)] 28 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 29 | pub struct Registry { 30 | sender: Sender, 31 | waker: MainThreadWakerImpl, 32 | } 33 | 34 | pub struct MainThreadRegistry { 35 | discoveries: Vec>>, 36 | sessions: Vec>, 37 | mocks: Vec>>, 38 | sender: Sender, 39 | receiver: Receiver, 40 | waker: MainThreadWakerImpl, 41 | grand_manager: LayerGrandManager, 42 | next_session_id: u32, 43 | } 44 | 45 | pub trait MainThreadWaker: 'static + Send { 46 | fn clone_box(&self) -> Box; 47 | fn wake(&self); 48 | } 49 | 50 | impl Clone for Box { 51 | fn clone(&self) -> Self { 52 | self.clone_box() 53 | } 54 | } 55 | 56 | #[derive(Clone)] 57 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 58 | struct MainThreadWakerImpl { 59 | #[cfg(feature = "ipc")] 60 | sender: Sender<()>, 61 | #[cfg(not(feature = "ipc"))] 62 | waker: Box, 63 | } 64 | 65 | #[cfg(feature = "ipc")] 66 | impl MainThreadWakerImpl { 67 | fn new(waker: Box) -> Result { 68 | let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?; 69 | ipc_channel::router::ROUTER.add_typed_route(receiver, Box::new(move |_| waker.wake())); 70 | Ok(MainThreadWakerImpl { sender }) 71 | } 72 | 73 | fn wake(&self) { 74 | let _ = self.sender.send(()); 75 | } 76 | } 77 | 78 | #[cfg(not(feature = "ipc"))] 79 | impl MainThreadWakerImpl { 80 | fn new(waker: Box) -> Result { 81 | Ok(MainThreadWakerImpl { waker }) 82 | } 83 | 84 | pub fn wake(&self) { 85 | self.waker.wake() 86 | } 87 | } 88 | 89 | impl Registry { 90 | pub fn supports_session(&mut self, mode: SessionMode, dest: Sender>) { 91 | let _ = self.sender.send(RegistryMsg::SupportsSession(mode, dest)); 92 | self.waker.wake(); 93 | } 94 | 95 | pub fn request_session( 96 | &mut self, 97 | mode: SessionMode, 98 | init: SessionInit, 99 | dest: Sender>, 100 | animation_frame_handler: Sender, 101 | ) { 102 | let _ = self.sender.send(RegistryMsg::RequestSession( 103 | mode, 104 | init, 105 | dest, 106 | animation_frame_handler, 107 | )); 108 | self.waker.wake(); 109 | } 110 | 111 | pub fn simulate_device_connection( 112 | &mut self, 113 | init: MockDeviceInit, 114 | dest: Sender, Error>>, 115 | ) { 116 | let _ = self 117 | .sender 118 | .send(RegistryMsg::SimulateDeviceConnection(init, dest)); 119 | self.waker.wake(); 120 | } 121 | } 122 | 123 | impl MainThreadRegistry { 124 | pub fn new( 125 | waker: Box, 126 | grand_manager: LayerGrandManager, 127 | ) -> Result { 128 | let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?; 129 | let discoveries = Vec::new(); 130 | let sessions = Vec::new(); 131 | let mocks = Vec::new(); 132 | let waker = MainThreadWakerImpl::new(waker)?; 133 | Ok(MainThreadRegistry { 134 | discoveries, 135 | sessions, 136 | mocks, 137 | sender, 138 | receiver, 139 | waker, 140 | grand_manager, 141 | next_session_id: 0, 142 | }) 143 | } 144 | 145 | pub fn registry(&self) -> Registry { 146 | Registry { 147 | sender: self.sender.clone(), 148 | waker: self.waker.clone(), 149 | } 150 | } 151 | 152 | pub fn register(&mut self, discovery: D) 153 | where 154 | D: DiscoveryAPI, 155 | { 156 | self.discoveries.push(Box::new(discovery)); 157 | } 158 | 159 | pub fn register_mock(&mut self, discovery: D) 160 | where 161 | D: MockDiscoveryAPI, 162 | { 163 | self.mocks.push(Box::new(discovery)); 164 | } 165 | 166 | pub fn run_on_main_thread(&mut self, session: S) 167 | where 168 | S: MainThreadSession, 169 | { 170 | self.sessions.push(Box::new(session)); 171 | } 172 | 173 | pub fn run_one_frame(&mut self) { 174 | while let Ok(msg) = self.receiver.try_recv() { 175 | self.handle_msg(msg); 176 | } 177 | for session in &mut self.sessions { 178 | session.run_one_frame(); 179 | } 180 | self.sessions.retain(|session| session.running()); 181 | } 182 | 183 | pub fn running(&self) -> bool { 184 | self.sessions.iter().any(|session| session.running()) 185 | } 186 | 187 | fn handle_msg(&mut self, msg: RegistryMsg) { 188 | match msg { 189 | RegistryMsg::SupportsSession(mode, dest) => { 190 | let _ = dest.send(self.supports_session(mode)); 191 | } 192 | RegistryMsg::RequestSession(mode, init, dest, raf_sender) => { 193 | let _ = dest.send(self.request_session(mode, init, raf_sender)); 194 | } 195 | RegistryMsg::SimulateDeviceConnection(init, dest) => { 196 | let _ = dest.send(self.simulate_device_connection(init)); 197 | } 198 | } 199 | } 200 | 201 | fn supports_session(&mut self, mode: SessionMode) -> Result<(), Error> { 202 | for discovery in &self.discoveries { 203 | if discovery.supports_session(mode) { 204 | return Ok(()); 205 | } 206 | } 207 | Err(Error::NoMatchingDevice) 208 | } 209 | 210 | fn request_session( 211 | &mut self, 212 | mode: SessionMode, 213 | init: SessionInit, 214 | raf_sender: Sender, 215 | ) -> Result { 216 | for discovery in &mut self.discoveries { 217 | if discovery.supports_session(mode) { 218 | let raf_sender = raf_sender.clone(); 219 | let id = SessionId(self.next_session_id); 220 | self.next_session_id += 1; 221 | let xr = SessionBuilder::new( 222 | &mut self.sessions, 223 | raf_sender, 224 | self.grand_manager.clone(), 225 | id, 226 | ); 227 | match discovery.request_session(mode, &init, xr) { 228 | Ok(session) => return Ok(session), 229 | Err(err) => warn!("XR device error {:?}", err), 230 | } 231 | } 232 | } 233 | warn!("no device could support the session"); 234 | Err(Error::NoMatchingDevice) 235 | } 236 | 237 | fn simulate_device_connection( 238 | &mut self, 239 | init: MockDeviceInit, 240 | ) -> Result, Error> { 241 | for mock in &mut self.mocks { 242 | let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?; 243 | if let Ok(discovery) = mock.simulate_device_connection(init.clone(), receiver) { 244 | self.discoveries.insert(0, discovery); 245 | return Ok(sender); 246 | } 247 | } 248 | Err(Error::NoMatchingDevice) 249 | } 250 | } 251 | 252 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 253 | enum RegistryMsg { 254 | RequestSession( 255 | SessionMode, 256 | SessionInit, 257 | Sender>, 258 | Sender, 259 | ), 260 | SupportsSession(SessionMode, Sender>), 261 | SimulateDeviceConnection(MockDeviceInit, Sender, Error>>), 262 | } 263 | -------------------------------------------------------------------------------- /webxr-api/session.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::channel; 6 | use crate::ContextId; 7 | use crate::DeviceAPI; 8 | use crate::Error; 9 | use crate::Event; 10 | use crate::Floor; 11 | use crate::Frame; 12 | use crate::FrameUpdateEvent; 13 | use crate::HitTestId; 14 | use crate::HitTestSource; 15 | use crate::InputSource; 16 | use crate::LayerGrandManager; 17 | use crate::LayerId; 18 | use crate::LayerInit; 19 | use crate::Native; 20 | use crate::Receiver; 21 | use crate::Sender; 22 | use crate::Viewport; 23 | use crate::Viewports; 24 | 25 | use euclid::Point2D; 26 | use euclid::Rect; 27 | use euclid::RigidTransform3D; 28 | use euclid::Size2D; 29 | 30 | use log::warn; 31 | 32 | use std::thread; 33 | use std::time::Duration; 34 | 35 | #[cfg(feature = "ipc")] 36 | use serde::{Deserialize, Serialize}; 37 | 38 | // How long to wait for an rAF. 39 | static TIMEOUT: Duration = Duration::from_millis(5); 40 | 41 | /// https://www.w3.org/TR/webxr/#xrsessionmode-enum 42 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 43 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 44 | pub enum SessionMode { 45 | Inline, 46 | ImmersiveVR, 47 | ImmersiveAR, 48 | } 49 | 50 | /// https://immersive-web.github.io/webxr/#dictdef-xrsessioninit 51 | #[derive(Clone, Debug, Eq, PartialEq)] 52 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 53 | pub struct SessionInit { 54 | pub required_features: Vec, 55 | pub optional_features: Vec, 56 | /// Secondary views are enabled with the `secondary-view` feature 57 | /// but for performance reasons we also ask users to enable this pref 58 | /// for now. 59 | pub first_person_observer_view: bool, 60 | } 61 | 62 | impl SessionInit { 63 | /// Helper function for validating a list of requested features against 64 | /// a list of supported features for a given mode 65 | pub fn validate(&self, mode: SessionMode, supported: &[String]) -> Result, Error> { 66 | for f in &self.required_features { 67 | // viewer and local in immersive are granted by default 68 | // https://immersive-web.github.io/webxr/#default-features 69 | if f == "viewer" || (f == "local" && mode != SessionMode::Inline) { 70 | continue; 71 | } 72 | 73 | if !supported.contains(f) { 74 | return Err(Error::UnsupportedFeature(f.into())); 75 | } 76 | } 77 | let mut granted = self.required_features.clone(); 78 | for f in &self.optional_features { 79 | if f == "viewer" 80 | || (f == "local" && mode != SessionMode::Inline) 81 | || supported.contains(f) 82 | { 83 | granted.push(f.clone()); 84 | } 85 | } 86 | 87 | Ok(granted) 88 | } 89 | 90 | pub fn feature_requested(&self, f: &str) -> bool { 91 | self.required_features 92 | .iter() 93 | .chain(self.optional_features.iter()) 94 | .find(|x| *x == f) 95 | .is_some() 96 | } 97 | } 98 | 99 | /// https://immersive-web.github.io/webxr-ar-module/#xrenvironmentblendmode-enum 100 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 101 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 102 | pub enum EnvironmentBlendMode { 103 | Opaque, 104 | AlphaBlend, 105 | Additive, 106 | } 107 | 108 | // The messages that are sent from the content thread to the session thread. 109 | #[derive(Debug)] 110 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 111 | enum SessionMsg { 112 | CreateLayer(ContextId, LayerInit, Sender>), 113 | DestroyLayer(ContextId, LayerId), 114 | SetLayers(Vec<(ContextId, LayerId)>), 115 | SetEventDest(Sender), 116 | UpdateClipPlanes(/* near */ f32, /* far */ f32), 117 | StartRenderLoop, 118 | RenderAnimationFrame, 119 | RequestHitTest(HitTestSource), 120 | CancelHitTest(HitTestId), 121 | UpdateFrameRate(f32, Sender), 122 | Quit, 123 | GetBoundsGeometry(Sender>>>), 124 | } 125 | 126 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 127 | #[derive(Clone)] 128 | pub struct Quitter { 129 | sender: Sender, 130 | } 131 | 132 | impl Quitter { 133 | pub fn quit(&self) { 134 | let _ = self.sender.send(SessionMsg::Quit); 135 | } 136 | } 137 | 138 | /// An object that represents an XR session. 139 | /// This is owned by the content thread. 140 | /// https://www.w3.org/TR/webxr/#xrsession-interface 141 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 142 | pub struct Session { 143 | floor_transform: Option>, 144 | viewports: Viewports, 145 | sender: Sender, 146 | environment_blend_mode: EnvironmentBlendMode, 147 | initial_inputs: Vec, 148 | granted_features: Vec, 149 | id: SessionId, 150 | supported_frame_rates: Vec, 151 | } 152 | 153 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 154 | #[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))] 155 | pub struct SessionId(pub(crate) u32); 156 | 157 | impl Session { 158 | pub fn id(&self) -> SessionId { 159 | self.id 160 | } 161 | 162 | pub fn floor_transform(&self) -> Option> { 163 | self.floor_transform.clone() 164 | } 165 | 166 | pub fn reference_space_bounds(&self) -> Option>> { 167 | let (sender, receiver) = channel().ok()?; 168 | let _ = self.sender.send(SessionMsg::GetBoundsGeometry(sender)); 169 | receiver.recv().ok()? 170 | } 171 | 172 | pub fn initial_inputs(&self) -> &[InputSource] { 173 | &self.initial_inputs 174 | } 175 | 176 | pub fn environment_blend_mode(&self) -> EnvironmentBlendMode { 177 | self.environment_blend_mode 178 | } 179 | 180 | pub fn viewports(&self) -> &[Rect] { 181 | &self.viewports.viewports 182 | } 183 | 184 | /// A resolution large enough to contain all the viewports. 185 | /// https://immersive-web.github.io/webxr/#recommended-webgl-framebuffer-resolution 186 | /// 187 | /// Returns None if the session is inline 188 | pub fn recommended_framebuffer_resolution(&self) -> Option> { 189 | self.viewports() 190 | .iter() 191 | .fold(None::>, |acc, vp| { 192 | Some(acc.map(|a| a.union(vp)).unwrap_or(*vp)) 193 | }) 194 | .map(|rect| Size2D::new(rect.max_x(), rect.max_y())) 195 | } 196 | 197 | pub fn create_layer(&self, context_id: ContextId, init: LayerInit) -> Result { 198 | let (sender, receiver) = channel().map_err(|_| Error::CommunicationError)?; 199 | let _ = self 200 | .sender 201 | .send(SessionMsg::CreateLayer(context_id, init, sender)); 202 | receiver.recv().map_err(|_| Error::CommunicationError)? 203 | } 204 | 205 | /// Destroy a layer 206 | pub fn destroy_layer(&self, context_id: ContextId, layer_id: LayerId) { 207 | let _ = self 208 | .sender 209 | .send(SessionMsg::DestroyLayer(context_id, layer_id)); 210 | } 211 | 212 | pub fn set_layers(&self, layers: Vec<(ContextId, LayerId)>) { 213 | let _ = self.sender.send(SessionMsg::SetLayers(layers)); 214 | } 215 | 216 | pub fn start_render_loop(&mut self) { 217 | let _ = self.sender.send(SessionMsg::StartRenderLoop); 218 | } 219 | 220 | pub fn update_clip_planes(&mut self, near: f32, far: f32) { 221 | let _ = self.sender.send(SessionMsg::UpdateClipPlanes(near, far)); 222 | } 223 | 224 | pub fn set_event_dest(&mut self, dest: Sender) { 225 | let _ = self.sender.send(SessionMsg::SetEventDest(dest)); 226 | } 227 | 228 | pub fn render_animation_frame(&mut self) { 229 | let _ = self.sender.send(SessionMsg::RenderAnimationFrame); 230 | } 231 | 232 | pub fn end_session(&mut self) { 233 | let _ = self.sender.send(SessionMsg::Quit); 234 | } 235 | 236 | pub fn apply_event(&mut self, event: FrameUpdateEvent) { 237 | match event { 238 | FrameUpdateEvent::UpdateFloorTransform(floor) => self.floor_transform = floor, 239 | FrameUpdateEvent::UpdateViewports(vp) => self.viewports = vp, 240 | FrameUpdateEvent::HitTestSourceAdded(_) => (), 241 | } 242 | } 243 | 244 | pub fn granted_features(&self) -> &[String] { 245 | &self.granted_features 246 | } 247 | 248 | pub fn request_hit_test(&self, source: HitTestSource) { 249 | let _ = self.sender.send(SessionMsg::RequestHitTest(source)); 250 | } 251 | 252 | pub fn cancel_hit_test(&self, id: HitTestId) { 253 | let _ = self.sender.send(SessionMsg::CancelHitTest(id)); 254 | } 255 | 256 | pub fn update_frame_rate(&mut self, rate: f32, sender: Sender) { 257 | let _ = self.sender.send(SessionMsg::UpdateFrameRate(rate, sender)); 258 | } 259 | 260 | pub fn supported_frame_rates(&self) -> &[f32] { 261 | &self.supported_frame_rates 262 | } 263 | } 264 | 265 | #[derive(PartialEq)] 266 | enum RenderState { 267 | NotInRenderLoop, 268 | InRenderLoop, 269 | PendingQuit, 270 | } 271 | 272 | /// For devices that want to do their own thread management, the `SessionThread` type is exposed. 273 | pub struct SessionThread { 274 | receiver: Receiver, 275 | sender: Sender, 276 | layers: Vec<(ContextId, LayerId)>, 277 | pending_layers: Option>, 278 | frame_count: u64, 279 | frame_sender: Sender, 280 | running: bool, 281 | device: Device, 282 | id: SessionId, 283 | render_state: RenderState, 284 | } 285 | 286 | impl SessionThread 287 | where 288 | Device: DeviceAPI, 289 | { 290 | pub fn new( 291 | mut device: Device, 292 | frame_sender: Sender, 293 | id: SessionId, 294 | ) -> Result { 295 | let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?; 296 | device.set_quitter(Quitter { 297 | sender: sender.clone(), 298 | }); 299 | let frame_count = 0; 300 | let running = true; 301 | let layers = Vec::new(); 302 | let pending_layers = None; 303 | Ok(SessionThread { 304 | sender, 305 | receiver, 306 | device, 307 | layers, 308 | pending_layers, 309 | frame_count, 310 | frame_sender, 311 | running, 312 | id, 313 | render_state: RenderState::NotInRenderLoop, 314 | }) 315 | } 316 | 317 | pub fn new_session(&mut self) -> Session { 318 | let floor_transform = self.device.floor_transform(); 319 | let viewports = self.device.viewports(); 320 | let sender = self.sender.clone(); 321 | let initial_inputs = self.device.initial_inputs(); 322 | let environment_blend_mode = self.device.environment_blend_mode(); 323 | let granted_features = self.device.granted_features().into(); 324 | let supported_frame_rates = self.device.supported_frame_rates(); 325 | Session { 326 | floor_transform, 327 | viewports, 328 | sender, 329 | initial_inputs, 330 | environment_blend_mode, 331 | granted_features, 332 | id: self.id, 333 | supported_frame_rates, 334 | } 335 | } 336 | 337 | pub fn run(&mut self) { 338 | loop { 339 | if let Ok(msg) = self.receiver.recv() { 340 | if !self.handle_msg(msg) { 341 | self.running = false; 342 | break; 343 | } 344 | } else { 345 | break; 346 | } 347 | } 348 | } 349 | 350 | fn handle_msg(&mut self, msg: SessionMsg) -> bool { 351 | log::debug!("processing {:?}", msg); 352 | match msg { 353 | SessionMsg::SetEventDest(dest) => { 354 | self.device.set_event_dest(dest); 355 | } 356 | SessionMsg::RequestHitTest(source) => { 357 | self.device.request_hit_test(source); 358 | } 359 | SessionMsg::CancelHitTest(id) => { 360 | self.device.cancel_hit_test(id); 361 | } 362 | SessionMsg::CreateLayer(context_id, layer_init, sender) => { 363 | let result = self.device.create_layer(context_id, layer_init); 364 | let _ = sender.send(result); 365 | } 366 | SessionMsg::DestroyLayer(context_id, layer_id) => { 367 | self.layers.retain(|&(_, other_id)| layer_id != other_id); 368 | self.device.destroy_layer(context_id, layer_id); 369 | } 370 | SessionMsg::SetLayers(layers) => { 371 | self.pending_layers = Some(layers); 372 | } 373 | SessionMsg::StartRenderLoop => { 374 | if let Some(layers) = self.pending_layers.take() { 375 | self.layers = layers; 376 | } 377 | let frame = match self.device.begin_animation_frame(&self.layers[..]) { 378 | Some(frame) => frame, 379 | None => { 380 | warn!("Device stopped providing frames, exiting"); 381 | return false; 382 | } 383 | }; 384 | self.render_state = RenderState::InRenderLoop; 385 | let _ = self.frame_sender.send(frame); 386 | } 387 | SessionMsg::UpdateClipPlanes(near, far) => self.device.update_clip_planes(near, far), 388 | SessionMsg::RenderAnimationFrame => { 389 | self.frame_count += 1; 390 | 391 | self.device.end_animation_frame(&self.layers[..]); 392 | 393 | if self.render_state == RenderState::PendingQuit { 394 | self.quit(); 395 | return false; 396 | } 397 | 398 | if let Some(layers) = self.pending_layers.take() { 399 | self.layers = layers; 400 | } 401 | #[allow(unused_mut)] 402 | let mut frame = match self.device.begin_animation_frame(&self.layers[..]) { 403 | Some(frame) => frame, 404 | None => { 405 | warn!("Device stopped providing frames, exiting"); 406 | return false; 407 | } 408 | }; 409 | 410 | let _ = self.frame_sender.send(frame); 411 | } 412 | SessionMsg::UpdateFrameRate(rate, sender) => { 413 | let new_framerate = self.device.update_frame_rate(rate); 414 | let _ = sender.send(new_framerate); 415 | } 416 | SessionMsg::Quit => { 417 | if self.render_state == RenderState::NotInRenderLoop { 418 | self.quit(); 419 | return false; 420 | } else { 421 | self.render_state = RenderState::PendingQuit; 422 | } 423 | } 424 | SessionMsg::GetBoundsGeometry(sender) => { 425 | let bounds = self.device.reference_space_bounds(); 426 | let _ = sender.send(bounds); 427 | } 428 | } 429 | true 430 | } 431 | 432 | fn quit(&mut self) { 433 | self.render_state = RenderState::NotInRenderLoop; 434 | self.device.quit(); 435 | } 436 | } 437 | 438 | /// Devices that need to can run sessions on the main thread. 439 | pub trait MainThreadSession: 'static { 440 | fn run_one_frame(&mut self); 441 | fn running(&self) -> bool; 442 | } 443 | 444 | impl MainThreadSession for SessionThread 445 | where 446 | Device: DeviceAPI, 447 | { 448 | fn run_one_frame(&mut self) { 449 | let frame_count = self.frame_count; 450 | while frame_count == self.frame_count && self.running { 451 | if let Ok(msg) = crate::recv_timeout(&self.receiver, TIMEOUT) { 452 | self.running = self.handle_msg(msg); 453 | } else { 454 | break; 455 | } 456 | } 457 | } 458 | 459 | fn running(&self) -> bool { 460 | self.running 461 | } 462 | } 463 | 464 | /// A type for building XR sessions 465 | pub struct SessionBuilder<'a, GL> { 466 | sessions: &'a mut Vec>, 467 | frame_sender: Sender, 468 | layer_grand_manager: LayerGrandManager, 469 | id: SessionId, 470 | } 471 | 472 | impl<'a, GL: 'static> SessionBuilder<'a, GL> { 473 | pub fn id(&self) -> SessionId { 474 | self.id 475 | } 476 | 477 | pub(crate) fn new( 478 | sessions: &'a mut Vec>, 479 | frame_sender: Sender, 480 | layer_grand_manager: LayerGrandManager, 481 | id: SessionId, 482 | ) -> Self { 483 | SessionBuilder { 484 | sessions, 485 | frame_sender, 486 | layer_grand_manager, 487 | id, 488 | } 489 | } 490 | 491 | /// For devices which are happy to hand over thread management to webxr. 492 | pub fn spawn(self, factory: Factory) -> Result 493 | where 494 | Factory: 'static + FnOnce(LayerGrandManager) -> Result + Send, 495 | Device: DeviceAPI, 496 | { 497 | let (acks, ackr) = crate::channel().or(Err(Error::CommunicationError))?; 498 | let frame_sender = self.frame_sender; 499 | let layer_grand_manager = self.layer_grand_manager; 500 | let id = self.id; 501 | thread::spawn(move || { 502 | match factory(layer_grand_manager) 503 | .and_then(|device| SessionThread::new(device, frame_sender, id)) 504 | { 505 | Ok(mut thread) => { 506 | let session = thread.new_session(); 507 | let _ = acks.send(Ok(session)); 508 | thread.run(); 509 | } 510 | Err(err) => { 511 | let _ = acks.send(Err(err)); 512 | } 513 | } 514 | }); 515 | ackr.recv().unwrap_or(Err(Error::CommunicationError)) 516 | } 517 | 518 | /// For devices that need to run on the main thread. 519 | pub fn run_on_main_thread(self, factory: Factory) -> Result 520 | where 521 | Factory: 'static + FnOnce(LayerGrandManager) -> Result, 522 | Device: DeviceAPI, 523 | { 524 | let device = factory(self.layer_grand_manager)?; 525 | let frame_sender = self.frame_sender; 526 | let mut session_thread = SessionThread::new(device, frame_sender, self.id)?; 527 | let session = session_thread.new_session(); 528 | self.sessions.push(Box::new(session_thread)); 529 | Ok(session) 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /webxr-api/space.rs: -------------------------------------------------------------------------------- 1 | use crate::InputId; 2 | use crate::Joint; 3 | use euclid::RigidTransform3D; 4 | 5 | #[derive(Clone, Copy, Debug)] 6 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 7 | /// A stand-in type for "the space isn't statically known since 8 | /// it comes from client side code" 9 | pub struct ApiSpace; 10 | 11 | #[derive(Clone, Copy, Debug, PartialEq)] 12 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 13 | pub enum BaseSpace { 14 | Local, 15 | Floor, 16 | Viewer, 17 | BoundedFloor, 18 | TargetRay(InputId), 19 | Grip(InputId), 20 | Joint(InputId, Joint), 21 | } 22 | 23 | #[derive(Clone, Copy, Debug)] 24 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 25 | pub struct Space { 26 | pub base: BaseSpace, 27 | pub offset: RigidTransform3D, 28 | } 29 | -------------------------------------------------------------------------------- /webxr-api/util.rs: -------------------------------------------------------------------------------- 1 | use crate::FrameUpdateEvent; 2 | use crate::HitTestId; 3 | use crate::HitTestSource; 4 | use euclid::Transform3D; 5 | 6 | #[derive(Clone, Copy, Debug)] 7 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 8 | pub struct ClipPlanes { 9 | pub near: f32, 10 | pub far: f32, 11 | /// Was there an update that needs propagation to the client? 12 | update: bool, 13 | } 14 | 15 | impl Default for ClipPlanes { 16 | fn default() -> Self { 17 | ClipPlanes { 18 | near: 0.1, 19 | far: 1000., 20 | update: false, 21 | } 22 | } 23 | } 24 | 25 | impl ClipPlanes { 26 | pub fn update(&mut self, near: f32, far: f32) { 27 | self.near = near; 28 | self.far = far; 29 | self.update = true; 30 | } 31 | 32 | /// Checks for and clears the pending update flag 33 | pub fn recently_updated(&mut self) -> bool { 34 | if self.update { 35 | self.update = false; 36 | true 37 | } else { 38 | false 39 | } 40 | } 41 | } 42 | 43 | #[derive(Clone, Debug, Default)] 44 | #[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))] 45 | /// Holds on to hit tests 46 | pub struct HitTestList { 47 | tests: Vec, 48 | uncommitted_tests: Vec, 49 | } 50 | 51 | impl HitTestList { 52 | pub fn request_hit_test(&mut self, source: HitTestSource) { 53 | self.uncommitted_tests.push(source) 54 | } 55 | 56 | pub fn commit_tests(&mut self) -> Vec { 57 | let mut events = vec![]; 58 | for test in self.uncommitted_tests.drain(..) { 59 | events.push(FrameUpdateEvent::HitTestSourceAdded(test.id)); 60 | self.tests.push(test); 61 | } 62 | events 63 | } 64 | 65 | pub fn tests(&self) -> &[HitTestSource] { 66 | &self.tests 67 | } 68 | 69 | pub fn cancel_hit_test(&mut self, id: HitTestId) { 70 | self.tests.retain(|s| s.id != id); 71 | self.uncommitted_tests.retain(|s| s.id != id); 72 | } 73 | } 74 | 75 | #[inline] 76 | /// Construct a projection matrix given the four angles from the center for the faces of the viewing frustum 77 | pub fn fov_to_projection_matrix( 78 | left: f32, 79 | right: f32, 80 | top: f32, 81 | bottom: f32, 82 | clip_planes: ClipPlanes, 83 | ) -> Transform3D { 84 | let near = clip_planes.near; 85 | // XXXManishearth deal with infinite planes 86 | let left = left.tan() * near; 87 | let right = right.tan() * near; 88 | let top = top.tan() * near; 89 | let bottom = bottom.tan() * near; 90 | 91 | frustum_to_projection_matrix(left, right, top, bottom, clip_planes) 92 | } 93 | 94 | #[inline] 95 | /// Construct matrix given the actual extent of the viewing frustum on the near plane 96 | pub fn frustum_to_projection_matrix( 97 | left: f32, 98 | right: f32, 99 | top: f32, 100 | bottom: f32, 101 | clip_planes: ClipPlanes, 102 | ) -> Transform3D { 103 | let near = clip_planes.near; 104 | let far = clip_planes.far; 105 | 106 | let w = right - left; 107 | let h = top - bottom; 108 | let d = far - near; 109 | 110 | // Column-major order 111 | Transform3D::new( 112 | 2. * near / w, 113 | 0., 114 | 0., 115 | 0., 116 | 0., 117 | 2. * near / h, 118 | 0., 119 | 0., 120 | (right + left) / w, 121 | (top + bottom) / h, 122 | -(far + near) / d, 123 | -1., 124 | 0., 125 | 0., 126 | -2. * far * near / d, 127 | 0., 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /webxr-api/view.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This crate uses `euclid`'s typed units, and exposes different coordinate spaces. 6 | 7 | use euclid::Rect; 8 | use euclid::RigidTransform3D; 9 | use euclid::Transform3D; 10 | 11 | #[cfg(feature = "ipc")] 12 | use serde::{Deserialize, Serialize}; 13 | 14 | use std::marker::PhantomData; 15 | 16 | /// The coordinate space of the viewer 17 | /// https://immersive-web.github.io/webxr/#dom-xrreferencespacetype-viewer 18 | #[derive(Clone, Copy, Debug)] 19 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 20 | pub enum Viewer {} 21 | 22 | /// The coordinate space of the floor 23 | /// https://immersive-web.github.io/webxr/#dom-xrreferencespacetype-local-floor 24 | #[derive(Clone, Copy, Debug)] 25 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 26 | pub enum Floor {} 27 | 28 | /// The coordinate space of the left eye 29 | /// https://immersive-web.github.io/webxr/#dom-xreye-left 30 | #[derive(Clone, Copy, Debug)] 31 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 32 | pub enum LeftEye {} 33 | 34 | /// The coordinate space of the right eye 35 | /// https://immersive-web.github.io/webxr/#dom-xreye-right 36 | #[derive(Clone, Copy, Debug)] 37 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 38 | pub enum RightEye {} 39 | 40 | /// The coordinate space of the left frustrum of a cubemap 41 | #[derive(Clone, Copy, Debug)] 42 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 43 | pub enum CubeLeft {} 44 | 45 | /// The coordinate space of the right frustrum of a cubemap 46 | #[derive(Clone, Copy, Debug)] 47 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 48 | pub enum CubeRight {} 49 | 50 | /// The coordinate space of the top frustrum of a cubemap 51 | #[derive(Clone, Copy, Debug)] 52 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 53 | pub enum CubeTop {} 54 | 55 | /// The coordinate space of the bottom frustrum of a cubemap 56 | #[derive(Clone, Copy, Debug)] 57 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 58 | pub enum CubeBottom {} 59 | 60 | /// The coordinate space of the back frustrum of a cubemap 61 | #[derive(Clone, Copy, Debug)] 62 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 63 | pub enum CubeBack {} 64 | 65 | /// Pattern-match on eyes 66 | #[derive(Clone, Copy, Debug)] 67 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 68 | pub struct SomeEye(u8, PhantomData); 69 | pub const LEFT_EYE: SomeEye = SomeEye(0, PhantomData); 70 | pub const RIGHT_EYE: SomeEye = SomeEye(1, PhantomData); 71 | pub const VIEWER: SomeEye = SomeEye(2, PhantomData); 72 | pub const CUBE_LEFT: SomeEye = SomeEye(3, PhantomData); 73 | pub const CUBE_RIGHT: SomeEye = SomeEye(4, PhantomData); 74 | pub const CUBE_TOP: SomeEye = SomeEye(5, PhantomData); 75 | pub const CUBE_BOTTOM: SomeEye = SomeEye(6, PhantomData); 76 | pub const CUBE_BACK: SomeEye = SomeEye(7, PhantomData); 77 | 78 | impl PartialEq> for SomeEye { 79 | fn eq(&self, rhs: &SomeEye) -> bool { 80 | self.0 == rhs.0 81 | } 82 | } 83 | 84 | /// The native 3D coordinate space of the device 85 | /// This is not part of the webvr specification. 86 | #[derive(Clone, Copy, Debug)] 87 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 88 | pub enum Native {} 89 | 90 | /// The normalized device coordinate space, where the display 91 | /// is from (-1,-1) to (1,1). 92 | // TODO: are we OK assuming that we can use the same coordinate system for all displays? 93 | #[derive(Clone, Copy, Debug)] 94 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 95 | pub enum Display {} 96 | 97 | /// The unnormalized device coordinate space, where the display 98 | /// is from (0,0) to (w,h), measured in pixels. 99 | // TODO: are we OK assuming that we can use the same coordinate system for all displays? 100 | #[derive(Clone, Copy, Debug)] 101 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 102 | pub enum Viewport {} 103 | 104 | /// The coordinate space of an input device 105 | #[derive(Clone, Copy, Debug)] 106 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 107 | pub enum Input {} 108 | 109 | /// The coordinate space of a secondary capture view 110 | #[derive(Clone, Copy, Debug)] 111 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 112 | pub enum Capture {} 113 | 114 | /// For each eye, the pose of that eye, 115 | /// its projection onto its display. 116 | /// For stereo displays, we have a `View` and a `View`. 117 | /// For mono displays, we hagve a `View` 118 | /// https://immersive-web.github.io/webxr/#xrview 119 | #[derive(Clone, Debug)] 120 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 121 | pub struct View { 122 | pub transform: RigidTransform3D, 123 | pub projection: Transform3D, 124 | } 125 | 126 | impl Default for View { 127 | fn default() -> Self { 128 | View { 129 | transform: RigidTransform3D::identity(), 130 | projection: Transform3D::identity(), 131 | } 132 | } 133 | } 134 | 135 | impl View { 136 | pub fn cast_unit(&self) -> View { 137 | View { 138 | transform: self.transform.cast_unit(), 139 | projection: Transform3D::from_untyped(&self.projection.to_untyped()), 140 | } 141 | } 142 | } 143 | 144 | /// Whether a device is mono or stereo, and the views it supports. 145 | #[derive(Clone, Debug)] 146 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 147 | pub enum Views { 148 | /// Mono view for inline VR, viewport and projection matrices are calculated by client 149 | Inline, 150 | Mono(View), 151 | Stereo(View, View), 152 | StereoCapture(View, View, View), 153 | Cubemap( 154 | View, 155 | View, 156 | View, 157 | View, 158 | View, 159 | View, 160 | ), 161 | } 162 | 163 | /// A list of viewports per-eye in the order of fields in Views. 164 | /// 165 | /// Not all must be in active use. 166 | #[derive(Clone, Debug)] 167 | #[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))] 168 | pub struct Viewports { 169 | pub viewports: Vec>, 170 | } 171 | -------------------------------------------------------------------------------- /webxr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webxr" 3 | version = "0.0.1" 4 | authors = ["The Servo Project Developers"] 5 | edition = "2018" 6 | 7 | homepage = "https://github.com/servo/webxr" 8 | repository = "https://github.com/servo/webxr" 9 | keywords = ["ar", "headset", "openxr", "vr", "webxr"] 10 | license = "MPL-2.0" 11 | 12 | description = '''A safe Rust API that provides a way to interact with 13 | virtual reality and augmented reality devices and integration with OpenXR. 14 | The API is inspired by the WebXR Device API (https://www.w3.org/TR/webxr/) 15 | but adapted to Rust design patterns.''' 16 | 17 | [lib] 18 | path = "lib.rs" 19 | 20 | [features] 21 | default = ["x11"] 22 | x11 = ["surfman/sm-x11"] 23 | angle = ["surfman/sm-angle"] 24 | glwindow = [] 25 | headless = [] 26 | ipc = ["webxr-api/ipc", "serde"] 27 | openxr-api = ["angle", "openxr", "winapi", "wio", "surfman/sm-angle-default"] 28 | 29 | [dependencies] 30 | webxr-api = { path = "../webxr-api" } 31 | crossbeam-channel = "0.5" 32 | euclid = "0.22" 33 | log = "0.4.6" 34 | openxr = { version = "0.19", optional = true } 35 | serde = { version = "1.0", optional = true } 36 | glow = "0.16" 37 | raw-window-handle = "0.6" 38 | surfman = { git = "https://github.com/servo/surfman", rev = "300789ddbda45c89e9165c31118bf1c4c07f89f6", features = [ 39 | "chains", 40 | "sm-raw-window-handle-06", 41 | ] } 42 | 43 | [target.'cfg(target_os = "windows")'.dependencies] 44 | winapi = { version = "0.3", features = [ 45 | "dxgi", 46 | "d3d11", 47 | "winerror", 48 | ], optional = true } 49 | wio = { version = "0.2", optional = true } 50 | -------------------------------------------------------------------------------- /webxr/gl_utils.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::SurfmanGL; 6 | use glow as gl; 7 | use glow::Context as Gl; 8 | use glow::HasContext; 9 | use std::collections::HashMap; 10 | use std::num::NonZero; 11 | use surfman::Device as SurfmanDevice; 12 | use webxr_api::ContextId; 13 | use webxr_api::GLContexts; 14 | use webxr_api::LayerId; 15 | 16 | pub(crate) fn framebuffer(framebuffer: u32) -> Option { 17 | NonZero::new(framebuffer).map(gl::NativeFramebuffer) 18 | } 19 | 20 | // A utility to clear a color texture and optional depth/stencil texture 21 | pub(crate) struct GlClearer { 22 | fbos: HashMap< 23 | ( 24 | LayerId, 25 | Option, 26 | Option, 27 | ), 28 | Option, 29 | >, 30 | should_reverse_winding: bool, 31 | } 32 | 33 | impl GlClearer { 34 | pub(crate) fn new(should_reverse_winding: bool) -> GlClearer { 35 | let fbos = HashMap::new(); 36 | GlClearer { 37 | fbos, 38 | should_reverse_winding, 39 | } 40 | } 41 | 42 | fn fbo( 43 | &mut self, 44 | gl: &Gl, 45 | layer_id: LayerId, 46 | color: Option, 47 | color_target: u32, 48 | depth_stencil: Option, 49 | ) -> Option { 50 | let should_reverse_winding = self.should_reverse_winding; 51 | *self 52 | .fbos 53 | .entry((layer_id, color, depth_stencil)) 54 | .or_insert_with(|| { 55 | // Save the current GL state 56 | let mut bound_fbos = [0, 0]; 57 | unsafe { 58 | gl.get_parameter_i32_slice(gl::DRAW_FRAMEBUFFER_BINDING, &mut bound_fbos[0..]); 59 | gl.get_parameter_i32_slice(gl::READ_FRAMEBUFFER_BINDING, &mut bound_fbos[1..]); 60 | 61 | // Generate and set attachments of a new FBO 62 | let fbo = gl.create_framebuffer().ok(); 63 | 64 | gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); 65 | gl.framebuffer_texture_2d( 66 | gl::FRAMEBUFFER, 67 | gl::COLOR_ATTACHMENT0, 68 | color_target, 69 | color, 70 | 0, 71 | ); 72 | gl.framebuffer_texture_2d( 73 | gl::FRAMEBUFFER, 74 | gl::DEPTH_STENCIL_ATTACHMENT, 75 | gl::TEXTURE_2D, 76 | depth_stencil, 77 | 0, 78 | ); 79 | 80 | // Necessary if using an OpenXR runtime that does not support mutable FOV, 81 | // as flipping the projection matrix necessitates reversing the winding order. 82 | if should_reverse_winding { 83 | gl.front_face(gl::CW); 84 | } 85 | 86 | // Restore the GL state 87 | gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, framebuffer(bound_fbos[0] as _)); 88 | gl.bind_framebuffer(gl::READ_FRAMEBUFFER, framebuffer(bound_fbos[1] as _)); 89 | debug_assert_eq!(gl.get_error(), gl::NO_ERROR); 90 | 91 | fbo 92 | } 93 | }) 94 | } 95 | 96 | pub(crate) fn clear( 97 | &mut self, 98 | device: &mut SurfmanDevice, 99 | contexts: &mut dyn GLContexts, 100 | context_id: ContextId, 101 | layer_id: LayerId, 102 | color: Option, 103 | color_target: u32, 104 | depth_stencil: Option, 105 | ) { 106 | let gl = match contexts.bindings(device, context_id) { 107 | None => return, 108 | Some(gl) => gl, 109 | }; 110 | let fbo = self.fbo(gl, layer_id, color, color_target, depth_stencil); 111 | unsafe { 112 | // Save the current GL state 113 | let mut bound_fbos = [0, 0]; 114 | let mut clear_color = [0., 0., 0., 0.]; 115 | let mut clear_depth = [0.]; 116 | let mut clear_stencil = [0]; 117 | let color_mask; 118 | let depth_mask; 119 | let mut stencil_mask = [0]; 120 | let scissor_enabled = gl.is_enabled(gl::SCISSOR_TEST); 121 | let rasterizer_enabled = gl.is_enabled(gl::RASTERIZER_DISCARD); 122 | 123 | gl.get_parameter_i32_slice(gl::DRAW_FRAMEBUFFER_BINDING, &mut bound_fbos[0..]); 124 | gl.get_parameter_i32_slice(gl::READ_FRAMEBUFFER_BINDING, &mut bound_fbos[1..]); 125 | gl.get_parameter_f32_slice(gl::COLOR_CLEAR_VALUE, &mut clear_color[..]); 126 | gl.get_parameter_f32_slice(gl::DEPTH_CLEAR_VALUE, &mut clear_depth[..]); 127 | gl.get_parameter_i32_slice(gl::STENCIL_CLEAR_VALUE, &mut clear_stencil[..]); 128 | depth_mask = gl.get_parameter_bool(gl::DEPTH_WRITEMASK); 129 | gl.get_parameter_i32_slice(gl::STENCIL_WRITEMASK, &mut stencil_mask[..]); 130 | color_mask = gl.get_parameter_bool_array::<4>(gl::COLOR_WRITEMASK); 131 | 132 | // Clear it 133 | gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); 134 | gl.clear_color(0., 0., 0., 1.); 135 | gl.clear_depth(1.); 136 | gl.clear_stencil(0); 137 | gl.disable(gl::SCISSOR_TEST); 138 | gl.disable(gl::RASTERIZER_DISCARD); 139 | gl.depth_mask(true); 140 | gl.stencil_mask(0xFFFFFFFF); 141 | gl.color_mask(true, true, true, true); 142 | gl.clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 143 | 144 | // Restore the GL state 145 | gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, framebuffer(bound_fbos[0] as _)); 146 | gl.bind_framebuffer(gl::READ_FRAMEBUFFER, framebuffer(bound_fbos[1] as _)); 147 | gl.clear_color( 148 | clear_color[0], 149 | clear_color[1], 150 | clear_color[2], 151 | clear_color[3], 152 | ); 153 | gl.color_mask(color_mask[0], color_mask[1], color_mask[2], color_mask[3]); 154 | gl.clear_depth(clear_depth[0] as f64); 155 | gl.clear_stencil(clear_stencil[0]); 156 | gl.depth_mask(depth_mask); 157 | gl.stencil_mask(stencil_mask[0] as _); 158 | if scissor_enabled { 159 | gl.enable(gl::SCISSOR_TEST); 160 | } 161 | if rasterizer_enabled { 162 | gl.enable(gl::RASTERIZER_DISCARD); 163 | } 164 | debug_assert_eq!(gl.get_error(), gl::NO_ERROR); 165 | } 166 | } 167 | 168 | pub(crate) fn destroy_layer( 169 | &mut self, 170 | device: &mut SurfmanDevice, 171 | contexts: &mut dyn GLContexts, 172 | context_id: ContextId, 173 | layer_id: LayerId, 174 | ) { 175 | let gl = match contexts.bindings(device, context_id) { 176 | None => return, 177 | Some(gl) => gl, 178 | }; 179 | self.fbos.retain(|&(other_id, _, _), &mut fbo| { 180 | if layer_id != other_id { 181 | true 182 | } else { 183 | if let Some(fbo) = fbo { 184 | unsafe { gl.delete_framebuffer(fbo) }; 185 | } 186 | false 187 | } 188 | }) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /webxr/headless/mod.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::SurfmanGL; 6 | use crate::SurfmanLayerManager; 7 | use euclid::{Point2D, RigidTransform3D}; 8 | use std::sync::{Arc, Mutex}; 9 | use std::thread; 10 | use surfman::chains::SwapChains; 11 | use webxr_api::util::{self, ClipPlanes, HitTestList}; 12 | use webxr_api::{ 13 | ApiSpace, BaseSpace, ContextId, DeviceAPI, DiscoveryAPI, Error, Event, EventBuffer, Floor, 14 | Frame, FrameUpdateEvent, HitTestId, HitTestResult, HitTestSource, Input, InputFrame, InputId, 15 | InputSource, LayerGrandManager, LayerId, LayerInit, LayerManager, MockButton, MockDeviceInit, 16 | MockDeviceMsg, MockDiscoveryAPI, MockInputMsg, MockViewInit, MockViewsInit, MockWorld, Native, 17 | Quitter, Ray, Receiver, SelectEvent, SelectKind, Sender, Session, SessionBuilder, SessionInit, 18 | SessionMode, Space, SubImages, View, Viewer, ViewerPose, Viewports, Views, 19 | }; 20 | 21 | pub struct HeadlessMockDiscovery {} 22 | 23 | struct HeadlessDiscovery { 24 | data: Arc>, 25 | supports_vr: bool, 26 | supports_inline: bool, 27 | supports_ar: bool, 28 | } 29 | 30 | struct InputInfo { 31 | source: InputSource, 32 | active: bool, 33 | pointer: Option>, 34 | grip: Option>, 35 | clicking: bool, 36 | buttons: Vec, 37 | } 38 | 39 | struct HeadlessDevice { 40 | data: Arc>, 41 | id: u32, 42 | hit_tests: HitTestList, 43 | granted_features: Vec, 44 | grand_manager: LayerGrandManager, 45 | layer_manager: Option, 46 | } 47 | 48 | struct PerSessionData { 49 | id: u32, 50 | mode: SessionMode, 51 | clip_planes: ClipPlanes, 52 | quitter: Option, 53 | events: EventBuffer, 54 | needs_vp_update: bool, 55 | } 56 | 57 | struct HeadlessDeviceData { 58 | floor_transform: Option>, 59 | viewer_origin: Option>, 60 | supported_features: Vec, 61 | views: MockViewsInit, 62 | needs_floor_update: bool, 63 | inputs: Vec, 64 | sessions: Vec, 65 | disconnected: bool, 66 | world: Option, 67 | next_id: u32, 68 | bounds_geometry: Vec>, 69 | } 70 | 71 | impl MockDiscoveryAPI for HeadlessMockDiscovery { 72 | fn simulate_device_connection( 73 | &mut self, 74 | init: MockDeviceInit, 75 | receiver: Receiver, 76 | ) -> Result>, Error> { 77 | let viewer_origin = init.viewer_origin.clone(); 78 | let floor_transform = init.floor_origin.map(|f| f.inverse()); 79 | let views = init.views.clone(); 80 | let data = HeadlessDeviceData { 81 | floor_transform, 82 | viewer_origin, 83 | supported_features: init.supported_features, 84 | views, 85 | needs_floor_update: false, 86 | inputs: vec![], 87 | sessions: vec![], 88 | disconnected: false, 89 | world: init.world, 90 | next_id: 0, 91 | bounds_geometry: vec![], 92 | }; 93 | let data = Arc::new(Mutex::new(data)); 94 | let data_ = data.clone(); 95 | 96 | thread::spawn(move || { 97 | run_loop(receiver, data_); 98 | }); 99 | Ok(Box::new(HeadlessDiscovery { 100 | data, 101 | supports_vr: init.supports_vr, 102 | supports_inline: init.supports_inline, 103 | supports_ar: init.supports_ar, 104 | })) 105 | } 106 | } 107 | 108 | fn run_loop(receiver: Receiver, data: Arc>) { 109 | while let Ok(msg) = receiver.recv() { 110 | if !data.lock().expect("Mutex poisoned").handle_msg(msg) { 111 | break; 112 | } 113 | } 114 | } 115 | 116 | impl DiscoveryAPI for HeadlessDiscovery { 117 | fn request_session( 118 | &mut self, 119 | mode: SessionMode, 120 | init: &SessionInit, 121 | xr: SessionBuilder, 122 | ) -> Result { 123 | if !self.supports_session(mode) { 124 | return Err(Error::NoMatchingDevice); 125 | } 126 | let data = self.data.clone(); 127 | let mut d = data.lock().unwrap(); 128 | let id = d.next_id; 129 | d.next_id += 1; 130 | let per_session = PerSessionData { 131 | id, 132 | mode, 133 | clip_planes: Default::default(), 134 | quitter: Default::default(), 135 | events: Default::default(), 136 | needs_vp_update: false, 137 | }; 138 | d.sessions.push(per_session); 139 | 140 | let granted_features = init.validate(mode, &d.supported_features)?; 141 | let layer_manager = None; 142 | drop(d); 143 | xr.spawn(move |grand_manager| { 144 | Ok(HeadlessDevice { 145 | data, 146 | id, 147 | granted_features, 148 | hit_tests: HitTestList::default(), 149 | grand_manager, 150 | layer_manager, 151 | }) 152 | }) 153 | } 154 | 155 | fn supports_session(&self, mode: SessionMode) -> bool { 156 | if self.data.lock().unwrap().disconnected { 157 | return false; 158 | } 159 | match mode { 160 | SessionMode::Inline => self.supports_inline, 161 | SessionMode::ImmersiveVR => self.supports_vr, 162 | SessionMode::ImmersiveAR => self.supports_ar, 163 | } 164 | } 165 | } 166 | 167 | fn view( 168 | init: MockViewInit, 169 | viewer: RigidTransform3D, 170 | clip_planes: ClipPlanes, 171 | ) -> View { 172 | let projection = if let Some((l, r, t, b)) = init.fov { 173 | util::fov_to_projection_matrix(l, r, t, b, clip_planes) 174 | } else { 175 | init.projection 176 | }; 177 | 178 | View { 179 | transform: init.transform.inverse().then(&viewer), 180 | projection, 181 | } 182 | } 183 | 184 | impl HeadlessDevice { 185 | fn with_per_session(&self, f: impl FnOnce(&mut PerSessionData) -> R) -> R { 186 | f(self 187 | .data 188 | .lock() 189 | .unwrap() 190 | .sessions 191 | .iter_mut() 192 | .find(|s| s.id == self.id) 193 | .unwrap()) 194 | } 195 | 196 | fn layer_manager(&mut self) -> Result<&mut LayerManager, Error> { 197 | if let Some(ref mut manager) = self.layer_manager { 198 | return Ok(manager); 199 | } 200 | let swap_chains = SwapChains::new(); 201 | let viewports = self.viewports(); 202 | let layer_manager = self.grand_manager.create_layer_manager(move |_, _| { 203 | Ok(SurfmanLayerManager::new(viewports, swap_chains)) 204 | })?; 205 | self.layer_manager = Some(layer_manager); 206 | Ok(self.layer_manager.as_mut().unwrap()) 207 | } 208 | } 209 | 210 | impl DeviceAPI for HeadlessDevice { 211 | fn floor_transform(&self) -> Option> { 212 | self.data.lock().unwrap().floor_transform.clone() 213 | } 214 | 215 | fn viewports(&self) -> Viewports { 216 | let d = self.data.lock().unwrap(); 217 | let per_session = d.sessions.iter().find(|s| s.id == self.id).unwrap(); 218 | d.viewports(per_session.mode) 219 | } 220 | 221 | fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result { 222 | self.layer_manager()?.create_layer(context_id, init) 223 | } 224 | 225 | fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) { 226 | self.layer_manager() 227 | .unwrap() 228 | .destroy_layer(context_id, layer_id) 229 | } 230 | 231 | fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option { 232 | let sub_images = self.layer_manager().ok()?.begin_frame(layers).ok()?; 233 | let mut data = self.data.lock().unwrap(); 234 | let mut frame = data.get_frame( 235 | data.sessions.iter().find(|s| s.id == self.id).unwrap(), 236 | sub_images, 237 | ); 238 | let per_session = data.sessions.iter_mut().find(|s| s.id == self.id).unwrap(); 239 | if per_session.needs_vp_update { 240 | per_session.needs_vp_update = false; 241 | let mode = per_session.mode; 242 | let vp = data.viewports(mode); 243 | frame.events.push(FrameUpdateEvent::UpdateViewports(vp)); 244 | } 245 | let events = self.hit_tests.commit_tests(); 246 | frame.events = events; 247 | 248 | if let Some(ref world) = data.world { 249 | for source in self.hit_tests.tests() { 250 | let ray = data.native_ray(source.ray, source.space); 251 | let ray = if let Some(ray) = ray { ray } else { break }; 252 | let hits = world 253 | .regions 254 | .iter() 255 | .filter(|region| source.types.is_type(region.ty)) 256 | .flat_map(|region| ®ion.faces) 257 | .filter_map(|triangle| triangle.intersect(ray)) 258 | .map(|space| HitTestResult { 259 | space, 260 | id: source.id, 261 | }); 262 | frame.hit_test_results.extend(hits); 263 | } 264 | } 265 | 266 | if data.needs_floor_update { 267 | frame.events.push(FrameUpdateEvent::UpdateFloorTransform( 268 | data.floor_transform.clone(), 269 | )); 270 | data.needs_floor_update = false; 271 | } 272 | Some(frame) 273 | } 274 | 275 | fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) { 276 | let _ = self.layer_manager().unwrap().end_frame(layers); 277 | thread::sleep(std::time::Duration::from_millis(20)); 278 | } 279 | 280 | fn initial_inputs(&self) -> Vec { 281 | vec![] 282 | } 283 | 284 | fn set_event_dest(&mut self, dest: Sender) { 285 | self.with_per_session(|s| s.events.upgrade(dest)) 286 | } 287 | 288 | fn quit(&mut self) { 289 | self.with_per_session(|s| s.events.callback(Event::SessionEnd)) 290 | } 291 | 292 | fn set_quitter(&mut self, quitter: Quitter) { 293 | self.with_per_session(|s| s.quitter = Some(quitter)) 294 | } 295 | 296 | fn update_clip_planes(&mut self, near: f32, far: f32) { 297 | self.with_per_session(|s| s.clip_planes.update(near, far)); 298 | } 299 | 300 | fn granted_features(&self) -> &[String] { 301 | &self.granted_features 302 | } 303 | 304 | fn request_hit_test(&mut self, source: HitTestSource) { 305 | self.hit_tests.request_hit_test(source) 306 | } 307 | 308 | fn cancel_hit_test(&mut self, id: HitTestId) { 309 | self.hit_tests.cancel_hit_test(id) 310 | } 311 | 312 | fn reference_space_bounds(&self) -> Option>> { 313 | let bounds = self.data.lock().unwrap().bounds_geometry.clone(); 314 | Some(bounds) 315 | } 316 | } 317 | 318 | impl HeadlessMockDiscovery { 319 | pub fn new() -> HeadlessMockDiscovery { 320 | HeadlessMockDiscovery {} 321 | } 322 | } 323 | 324 | macro_rules! with_all_sessions { 325 | ($self:ident, |$s:ident| $e:expr) => { 326 | for $s in &mut $self.sessions { 327 | $e; 328 | } 329 | }; 330 | } 331 | 332 | impl HeadlessDeviceData { 333 | fn get_frame(&self, s: &PerSessionData, sub_images: Vec) -> Frame { 334 | let views = self.views.clone(); 335 | 336 | let pose = self.viewer_origin.map(|transform| { 337 | let views = if s.mode == SessionMode::Inline { 338 | Views::Inline 339 | } else { 340 | match views { 341 | MockViewsInit::Mono(one) => Views::Mono(view(one, transform, s.clip_planes)), 342 | MockViewsInit::Stereo(one, two) => Views::Stereo( 343 | view(one, transform, s.clip_planes), 344 | view(two, transform, s.clip_planes), 345 | ), 346 | } 347 | }; 348 | 349 | ViewerPose { transform, views } 350 | }); 351 | let inputs = self 352 | .inputs 353 | .iter() 354 | .filter(|i| i.active) 355 | .map(|i| InputFrame { 356 | id: i.source.id, 357 | target_ray_origin: i.pointer, 358 | grip_origin: i.grip, 359 | pressed: false, 360 | squeezed: false, 361 | hand: None, 362 | button_values: vec![], 363 | axis_values: vec![], 364 | input_changed: false, 365 | }) 366 | .collect(); 367 | Frame { 368 | pose, 369 | inputs, 370 | events: vec![], 371 | sub_images, 372 | hit_test_results: vec![], 373 | predicted_display_time: 0.0, 374 | } 375 | } 376 | 377 | fn viewports(&self, mode: SessionMode) -> Viewports { 378 | let vec = if mode == SessionMode::Inline { 379 | vec![] 380 | } else { 381 | match &self.views { 382 | MockViewsInit::Mono(one) => vec![one.viewport], 383 | MockViewsInit::Stereo(one, two) => vec![one.viewport, two.viewport], 384 | } 385 | }; 386 | Viewports { viewports: vec } 387 | } 388 | 389 | fn trigger_select(&mut self, id: InputId, kind: SelectKind, event: SelectEvent) { 390 | for i in 0..self.sessions.len() { 391 | let frame = self.get_frame(&self.sessions[i], Vec::new()); 392 | self.sessions[i] 393 | .events 394 | .callback(Event::Select(id, kind, event, frame)); 395 | } 396 | } 397 | 398 | fn handle_msg(&mut self, msg: MockDeviceMsg) -> bool { 399 | match msg { 400 | MockDeviceMsg::SetWorld(w) => self.world = Some(w), 401 | MockDeviceMsg::ClearWorld => self.world = None, 402 | MockDeviceMsg::SetViewerOrigin(viewer_origin) => { 403 | self.viewer_origin = viewer_origin; 404 | } 405 | MockDeviceMsg::SetFloorOrigin(floor_origin) => { 406 | self.floor_transform = floor_origin.map(|f| f.inverse()); 407 | self.needs_floor_update = true; 408 | } 409 | MockDeviceMsg::SetViews(views) => { 410 | self.views = views; 411 | with_all_sessions!(self, |s| { 412 | s.needs_vp_update = true; 413 | }) 414 | } 415 | MockDeviceMsg::VisibilityChange(v) => { 416 | with_all_sessions!(self, |s| s.events.callback(Event::VisibilityChange(v))) 417 | } 418 | MockDeviceMsg::AddInputSource(init) => { 419 | self.inputs.push(InputInfo { 420 | source: init.source.clone(), 421 | pointer: init.pointer_origin, 422 | grip: init.grip_origin, 423 | active: true, 424 | clicking: false, 425 | buttons: init.supported_buttons, 426 | }); 427 | with_all_sessions!(self, |s| s 428 | .events 429 | .callback(Event::AddInput(init.source.clone()))) 430 | } 431 | MockDeviceMsg::MessageInputSource(id, msg) => { 432 | if let Some(ref mut input) = self.inputs.iter_mut().find(|i| i.source.id == id) { 433 | match msg { 434 | MockInputMsg::SetHandedness(h) => { 435 | input.source.handedness = h; 436 | with_all_sessions!(self, |s| { 437 | s.events 438 | .callback(Event::UpdateInput(id, input.source.clone())) 439 | }); 440 | } 441 | MockInputMsg::SetProfiles(p) => { 442 | input.source.profiles = p; 443 | with_all_sessions!(self, |s| { 444 | s.events 445 | .callback(Event::UpdateInput(id, input.source.clone())) 446 | }); 447 | } 448 | MockInputMsg::SetTargetRayMode(t) => { 449 | input.source.target_ray_mode = t; 450 | with_all_sessions!(self, |s| { 451 | s.events 452 | .callback(Event::UpdateInput(id, input.source.clone())) 453 | }); 454 | } 455 | MockInputMsg::SetPointerOrigin(p) => input.pointer = p, 456 | MockInputMsg::SetGripOrigin(p) => input.grip = p, 457 | MockInputMsg::TriggerSelect(kind, event) => { 458 | if !input.active { 459 | return true; 460 | } 461 | let clicking = input.clicking; 462 | input.clicking = event == SelectEvent::Start; 463 | match event { 464 | SelectEvent::Start => { 465 | self.trigger_select(id, kind, event); 466 | } 467 | SelectEvent::End => { 468 | if clicking { 469 | self.trigger_select(id, kind, SelectEvent::Select); 470 | } else { 471 | self.trigger_select(id, kind, SelectEvent::End); 472 | } 473 | } 474 | SelectEvent::Select => { 475 | self.trigger_select(id, kind, SelectEvent::Start); 476 | self.trigger_select(id, kind, SelectEvent::Select); 477 | } 478 | } 479 | } 480 | MockInputMsg::Disconnect => { 481 | if input.active { 482 | with_all_sessions!(self, |s| s 483 | .events 484 | .callback(Event::RemoveInput(input.source.id))); 485 | input.active = false; 486 | input.clicking = false; 487 | } 488 | } 489 | MockInputMsg::Reconnect => { 490 | if !input.active { 491 | with_all_sessions!(self, |s| s 492 | .events 493 | .callback(Event::AddInput(input.source.clone()))); 494 | input.active = true; 495 | } 496 | } 497 | MockInputMsg::SetSupportedButtons(buttons) => { 498 | input.buttons = buttons; 499 | with_all_sessions!(self, |s| s.events.callback(Event::UpdateInput( 500 | input.source.id, 501 | input.source.clone() 502 | ))); 503 | } 504 | MockInputMsg::UpdateButtonState(state) => { 505 | if let Some(button) = input 506 | .buttons 507 | .iter_mut() 508 | .find(|b| b.button_type == state.button_type) 509 | { 510 | *button = state; 511 | } 512 | } 513 | } 514 | } 515 | } 516 | MockDeviceMsg::Disconnect(s) => { 517 | self.disconnected = true; 518 | with_all_sessions!(self, |s| s.quitter.as_ref().map(|q| q.quit())); 519 | // notify the client that we're done disconnecting 520 | let _ = s.send(()); 521 | return false; 522 | } 523 | MockDeviceMsg::SetBoundsGeometry(g) => { 524 | self.bounds_geometry = g; 525 | } 526 | MockDeviceMsg::SimulateResetPose => { 527 | with_all_sessions!(self, |s| s.events.callback(Event::ReferenceSpaceChanged( 528 | BaseSpace::Local, 529 | RigidTransform3D::identity() 530 | ))); 531 | } 532 | } 533 | true 534 | } 535 | 536 | fn native_ray(&self, ray: Ray, space: Space) -> Option> { 537 | let origin: RigidTransform3D = match space.base { 538 | BaseSpace::Local => RigidTransform3D::identity(), 539 | BaseSpace::Floor => self.floor_transform?.inverse().cast_unit(), 540 | BaseSpace::Viewer => self.viewer_origin?.cast_unit(), 541 | BaseSpace::BoundedFloor => self.floor_transform?.inverse().cast_unit(), 542 | BaseSpace::TargetRay(id) => self 543 | .inputs 544 | .iter() 545 | .find(|i| i.source.id == id)? 546 | .pointer? 547 | .cast_unit(), 548 | BaseSpace::Grip(id) => self 549 | .inputs 550 | .iter() 551 | .find(|i| i.source.id == id)? 552 | .grip? 553 | .cast_unit(), 554 | BaseSpace::Joint(..) => panic!("Cannot request mocking backend with hands"), 555 | }; 556 | let space_origin = space.offset.then(&origin); 557 | 558 | let origin_rigid: RigidTransform3D = ray.origin.into(); 559 | Some(Ray { 560 | origin: origin_rigid.then(&space_origin).translation, 561 | direction: space_origin.rotation.transform_vector3d(ray.direction), 562 | }) 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /webxr/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This crate defines the Rust implementation of WebXR for various devices. 6 | 7 | #[cfg(feature = "glwindow")] 8 | pub mod glwindow; 9 | 10 | #[cfg(feature = "headless")] 11 | pub mod headless; 12 | 13 | #[cfg(feature = "openxr-api")] 14 | pub mod openxr; 15 | 16 | pub mod surfman_layer_manager; 17 | pub use surfman_layer_manager::SurfmanGL; 18 | pub use surfman_layer_manager::SurfmanLayerManager; 19 | pub type MainThreadRegistry = webxr_api::MainThreadRegistry; 20 | pub type Discovery = Box>; 21 | 22 | pub(crate) mod gl_utils; 23 | -------------------------------------------------------------------------------- /webxr/openxr/graphics.rs: -------------------------------------------------------------------------------- 1 | use euclid::{Size2D, UnknownUnit}; 2 | use openxr::{ExtensionSet, FrameStream, FrameWaiter, Graphics, Instance, Session, SystemId}; 3 | use surfman::Context as SurfmanContext; 4 | use surfman::Device as SurfmanDevice; 5 | use surfman::Error as SurfmanError; 6 | use surfman::SurfaceTexture; 7 | use webxr_api::Error; 8 | 9 | pub enum GraphicsProvider {} 10 | 11 | pub trait GraphicsProviderMethods { 12 | fn enable_graphics_extensions(exts: &mut ExtensionSet); 13 | fn pick_format(formats: &[u32]) -> u32; 14 | fn create_session( 15 | device: &SurfmanDevice, 16 | instance: &Instance, 17 | system: SystemId, 18 | ) -> Result<(Session, FrameWaiter, FrameStream), Error>; 19 | fn surface_texture_from_swapchain_texture( 20 | image: ::SwapchainImage, 21 | device: &mut SurfmanDevice, 22 | context: &mut SurfmanContext, 23 | size: &Size2D, 24 | ) -> Result; 25 | } 26 | -------------------------------------------------------------------------------- /webxr/openxr/graphics_d3d11.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, ptr}; 2 | 3 | use euclid::{Size2D, UnknownUnit}; 4 | use log::warn; 5 | use openxr::d3d::{Requirements, SessionCreateInfoD3D11, D3D11}; 6 | use openxr::{ 7 | ExtensionSet, FormFactor, FrameStream, FrameWaiter, Graphics, Instance, Session, SystemId, 8 | }; 9 | use surfman::Adapter as SurfmanAdapter; 10 | use surfman::Context as SurfmanContext; 11 | use surfman::Device as SurfmanDevice; 12 | use surfman::Error as SurfmanError; 13 | use surfman::SurfaceTexture; 14 | use webxr_api::Error; 15 | use winapi::shared::winerror::{DXGI_ERROR_NOT_FOUND, S_OK}; 16 | use winapi::shared::{dxgi, dxgiformat}; 17 | use winapi::um::d3d11::ID3D11Texture2D; 18 | use winapi::Interface; 19 | use wio::com::ComPtr; 20 | 21 | use crate::openxr::graphics::{GraphicsProvider, GraphicsProviderMethods}; 22 | use crate::openxr::{create_instance, AppInfo}; 23 | 24 | pub type Backend = D3D11; 25 | 26 | impl GraphicsProviderMethods for GraphicsProvider { 27 | fn enable_graphics_extensions(exts: &mut ExtensionSet) { 28 | exts.khr_d3d11_enable = true; 29 | } 30 | 31 | fn pick_format(formats: &[u32]) -> u32 { 32 | // TODO: extract the format from surfman's device and pick a matching 33 | // valid format based on that. For now, assume that eglChooseConfig will 34 | // gravitate to B8G8R8A8. 35 | warn!("Available formats: {:?}", formats); 36 | for format in formats { 37 | match *format { 38 | dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => return *format, 39 | dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM => return *format, 40 | //dxgiformat::DXGI_FORMAT_R8G8B8A8_UNORM => return *format, 41 | f => { 42 | warn!("Backend requested unsupported format {:?}", f); 43 | } 44 | } 45 | } 46 | 47 | panic!("No formats supported amongst {:?}", formats); 48 | } 49 | 50 | fn create_session( 51 | device: &SurfmanDevice, 52 | instance: &Instance, 53 | system: SystemId, 54 | ) -> Result<(Session, FrameWaiter, FrameStream), Error> { 55 | // Get the current surfman device and extract its D3D device. This will ensure 56 | // that the OpenXR runtime's texture will be shareable with surfman's surfaces. 57 | let native_device = device.native_device(); 58 | let d3d_device = native_device.d3d11_device; 59 | 60 | // FIXME: we should be using these graphics requirements to drive the actual 61 | // d3d device creation, rather than assuming the device that surfman 62 | // already created is appropriate. OpenXR returns a validation error 63 | // unless we call this method, so we call it and ignore the results 64 | // in the short term. 65 | let _requirements = D3D11::requirements(&instance, system) 66 | .map_err(|e| Error::BackendSpecific(format!("D3D11::requirements {:?}", e)))?; 67 | 68 | unsafe { 69 | instance 70 | .create_session::( 71 | system, 72 | &SessionCreateInfoD3D11 { 73 | device: d3d_device as *mut _, 74 | }, 75 | ) 76 | .map_err(|e| Error::BackendSpecific(format!("Instance::create_session {:?}", e))) 77 | } 78 | } 79 | 80 | fn surface_texture_from_swapchain_texture( 81 | image: ::SwapchainImage, 82 | device: &mut SurfmanDevice, 83 | context: &mut SurfmanContext, 84 | size: &Size2D, 85 | ) -> Result { 86 | unsafe { 87 | let image = ComPtr::from_raw(image as *mut ID3D11Texture2D); 88 | image.AddRef(); 89 | device.create_surface_texture_from_texture(context, size, image) 90 | } 91 | } 92 | } 93 | 94 | fn get_matching_adapter( 95 | requirements: &Requirements, 96 | ) -> Result, String> { 97 | unsafe { 98 | let mut factory_ptr: *mut dxgi::IDXGIFactory1 = ptr::null_mut(); 99 | let result = dxgi::CreateDXGIFactory1( 100 | &dxgi::IDXGIFactory1::uuidof(), 101 | &mut factory_ptr as *mut _ as *mut _, 102 | ); 103 | assert_eq!(result, S_OK); 104 | let factory = ComPtr::from_raw(factory_ptr); 105 | 106 | let index = 0; 107 | loop { 108 | let mut adapter_ptr = ptr::null_mut(); 109 | let result = factory.EnumAdapters1(index, &mut adapter_ptr); 110 | if result == DXGI_ERROR_NOT_FOUND { 111 | return Err("No matching adapter".to_owned()); 112 | } 113 | assert_eq!(result, S_OK); 114 | let adapter = ComPtr::from_raw(adapter_ptr); 115 | let mut adapter_desc = mem::zeroed(); 116 | let result = adapter.GetDesc1(&mut adapter_desc); 117 | assert_eq!(result, S_OK); 118 | let adapter_luid = &adapter_desc.AdapterLuid; 119 | if adapter_luid.LowPart == requirements.adapter_luid.LowPart 120 | && adapter_luid.HighPart == requirements.adapter_luid.HighPart 121 | { 122 | return Ok(adapter); 123 | } 124 | } 125 | } 126 | } 127 | 128 | #[allow(unused)] 129 | pub fn create_surfman_adapter() -> Option { 130 | let instance = create_instance(false, false, false, &AppInfo::default()).ok()?; 131 | let system = instance 132 | .instance 133 | .system(FormFactor::HEAD_MOUNTED_DISPLAY) 134 | .ok()?; 135 | 136 | let requirements = D3D11::requirements(&instance.instance, system).ok()?; 137 | let adapter = get_matching_adapter(&requirements).ok()?; 138 | Some(SurfmanAdapter::from_dxgi_adapter(adapter.up())) 139 | } 140 | -------------------------------------------------------------------------------- /webxr/openxr/input.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | use std::mem::MaybeUninit; 3 | 4 | use euclid::RigidTransform3D; 5 | use log::debug; 6 | use openxr::sys::{ 7 | HandJointLocationsEXT, HandJointsLocateInfoEXT, HandTrackingAimStateFB, 8 | FB_HAND_TRACKING_AIM_EXTENSION_NAME, 9 | }; 10 | use openxr::{ 11 | self, Action, ActionSet, Binding, FrameState, Graphics, Hand as HandEnum, HandJoint, 12 | HandJointLocation, HandTracker, HandTrackingAimFlagsFB, Instance, Path, Posef, Session, Space, 13 | SpaceLocationFlags, HAND_JOINT_COUNT, 14 | }; 15 | use webxr_api::Finger; 16 | use webxr_api::Hand; 17 | use webxr_api::Handedness; 18 | use webxr_api::Input; 19 | use webxr_api::InputFrame; 20 | use webxr_api::InputId; 21 | use webxr_api::InputSource; 22 | use webxr_api::JointFrame; 23 | use webxr_api::Native; 24 | use webxr_api::SelectEvent; 25 | use webxr_api::TargetRayMode; 26 | use webxr_api::Viewer; 27 | 28 | use super::interaction_profiles::InteractionProfile; 29 | use super::IDENTITY_POSE; 30 | 31 | use crate::ext_string; 32 | use crate::openxr::interaction_profiles::INTERACTION_PROFILES; 33 | 34 | /// Number of frames to wait with the menu gesture before 35 | /// opening the menu. 36 | const MENU_GESTURE_SUSTAIN_THRESHOLD: u8 = 60; 37 | 38 | /// Helper macro for binding action paths in an interaction profile entry 39 | macro_rules! bind_inputs { 40 | ($actions:expr, $paths:expr, $hand:expr, $instance:expr, $ret:expr) => { 41 | $actions.iter().enumerate().for_each(|(i, action)| { 42 | let action_path = $paths[i]; 43 | if action_path != "" { 44 | let path = $instance 45 | .string_to_path(&format!("/user/hand/{}/input/{}", $hand, action_path)) 46 | .expect(&format!( 47 | "Failed to create path for /user/hand/{}/input/{}", 48 | $hand, action_path 49 | )); 50 | let binding = Binding::new(action, path); 51 | $ret.push(binding); 52 | } 53 | }); 54 | }; 55 | } 56 | 57 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 58 | enum ClickState { 59 | Clicking, 60 | Done, 61 | } 62 | 63 | /// All the information on a single input frame 64 | pub struct Frame { 65 | pub frame: InputFrame, 66 | pub select: Option, 67 | pub squeeze: Option, 68 | pub menu_selected: bool, 69 | } 70 | 71 | impl ClickState { 72 | fn update_from_action( 73 | &mut self, 74 | action: &Action, 75 | session: &Session, 76 | menu_selected: bool, 77 | ) -> (/* is_active */ bool, Option) { 78 | let click = action.state(session, Path::NULL).unwrap(); 79 | 80 | let select_event = 81 | self.update_from_value(click.current_state, click.is_active, menu_selected); 82 | 83 | (click.is_active, select_event) 84 | } 85 | 86 | fn update_from_value( 87 | &mut self, 88 | current_state: bool, 89 | is_active: bool, 90 | menu_selected: bool, 91 | ) -> Option { 92 | if is_active { 93 | match (current_state, *self) { 94 | (_, ClickState::Clicking) if menu_selected => { 95 | *self = ClickState::Done; 96 | // Cancel the select, we're showing a menu 97 | Some(SelectEvent::End) 98 | } 99 | (true, ClickState::Done) => { 100 | *self = ClickState::Clicking; 101 | Some(SelectEvent::Start) 102 | } 103 | (false, ClickState::Clicking) => { 104 | *self = ClickState::Done; 105 | Some(SelectEvent::Select) 106 | } 107 | _ => None, 108 | } 109 | } else if *self == ClickState::Clicking { 110 | *self = ClickState::Done; 111 | // Cancel the select, we lost tracking 112 | Some(SelectEvent::End) 113 | } else { 114 | None 115 | } 116 | } 117 | } 118 | 119 | pub struct OpenXRInput { 120 | id: InputId, 121 | action_aim_pose: Action, 122 | action_aim_space: Space, 123 | action_grip_pose: Action, 124 | action_grip_space: Space, 125 | action_click: Action, 126 | action_squeeze: Action, 127 | handedness: Handedness, 128 | click_state: ClickState, 129 | squeeze_state: ClickState, 130 | menu_gesture_sustain: u8, 131 | #[allow(unused)] 132 | hand_tracker: Option, 133 | action_buttons_common: Vec>, 134 | action_buttons_left: Vec>, 135 | action_buttons_right: Vec>, 136 | action_axes_common: Vec>, 137 | use_alternate_input_source: bool, 138 | } 139 | 140 | fn hand_str(h: Handedness) -> &'static str { 141 | match h { 142 | Handedness::Right => "right", 143 | Handedness::Left => "left", 144 | _ => panic!("We don't support unknown handedness in openxr"), 145 | } 146 | } 147 | 148 | impl OpenXRInput { 149 | pub fn new( 150 | id: InputId, 151 | handedness: Handedness, 152 | action_set: &ActionSet, 153 | session: &Session, 154 | needs_hands: bool, 155 | supported_interaction_profiles: Vec<&'static str>, 156 | ) -> Self { 157 | let hand = hand_str(handedness); 158 | let action_aim_pose: Action = action_set 159 | .create_action( 160 | &format!("{}_hand_aim", hand), 161 | &format!("{} hand aim", hand), 162 | &[], 163 | ) 164 | .unwrap(); 165 | let action_aim_space = action_aim_pose 166 | .create_space(session.clone(), Path::NULL, IDENTITY_POSE) 167 | .unwrap(); 168 | let action_grip_pose: Action = action_set 169 | .create_action( 170 | &format!("{}_hand_grip", hand), 171 | &format!("{} hand grip", hand), 172 | &[], 173 | ) 174 | .unwrap(); 175 | let action_grip_space = action_grip_pose 176 | .create_space(session.clone(), Path::NULL, IDENTITY_POSE) 177 | .unwrap(); 178 | let action_click: Action = action_set 179 | .create_action( 180 | &format!("{}_hand_click", hand), 181 | &format!("{} hand click", hand), 182 | &[], 183 | ) 184 | .unwrap(); 185 | let action_squeeze: Action = action_set 186 | .create_action( 187 | &format!("{}_hand_squeeze", hand), 188 | &format!("{} hand squeeze", hand), 189 | &[], 190 | ) 191 | .unwrap(); 192 | 193 | let hand_tracker = if needs_hands { 194 | let hand = match handedness { 195 | Handedness::Left => HandEnum::LEFT, 196 | Handedness::Right => HandEnum::RIGHT, 197 | _ => panic!("We don't support unknown handedness in openxr"), 198 | }; 199 | session.create_hand_tracker(hand).ok() 200 | } else { 201 | None 202 | }; 203 | 204 | let action_buttons_common: Vec> = { 205 | let button1: Action = action_set 206 | .create_action( 207 | &format!("{}_trigger", hand), 208 | &format!("{}_trigger", hand), 209 | &[], 210 | ) 211 | .unwrap(); 212 | let button2: Action = action_set 213 | .create_action(&format!("{}_grip", hand), &format!("{}_grip", hand), &[]) 214 | .unwrap(); 215 | let button3: Action = action_set 216 | .create_action( 217 | &format!("{}_touchpad_click", hand), 218 | &format!("{}_touchpad_click", hand), 219 | &[], 220 | ) 221 | .unwrap(); 222 | let button4: Action = action_set 223 | .create_action( 224 | &format!("{}_thumbstick_click", hand), 225 | &format!("{}_thumbstick_click", hand), 226 | &[], 227 | ) 228 | .unwrap(); 229 | vec![button1, button2, button3, button4] 230 | }; 231 | 232 | let action_buttons_left = { 233 | let button1: Action = action_set 234 | .create_action(&format!("{}_x", hand), &format!("{}_x", hand), &[]) 235 | .unwrap(); 236 | let button2: Action = action_set 237 | .create_action(&format!("{}_y", hand), &format!("{}_y", hand), &[]) 238 | .unwrap(); 239 | vec![button1, button2] 240 | }; 241 | 242 | let action_buttons_right = { 243 | let button1: Action = action_set 244 | .create_action(&format!("{}_a", hand), &format!("{}_a", hand), &[]) 245 | .unwrap(); 246 | let button2: Action = action_set 247 | .create_action(&format!("{}_b", hand), &format!("{}_b", hand), &[]) 248 | .unwrap(); 249 | vec![button1, button2] 250 | }; 251 | 252 | let action_axes_common: Vec> = { 253 | let axis1: Action = action_set 254 | .create_action( 255 | &format!("{}_touchpad_x", hand), 256 | &format!("{}_touchpad_x", hand), 257 | &[], 258 | ) 259 | .unwrap(); 260 | let axis2: Action = action_set 261 | .create_action( 262 | &format!("{}_touchpad_y", hand), 263 | &format!("{}_touchpad_y", hand), 264 | &[], 265 | ) 266 | .unwrap(); 267 | let axis3: Action = action_set 268 | .create_action( 269 | &format!("{}_thumbstick_x", hand), 270 | &format!("{}_thumbstick_x", hand), 271 | &[], 272 | ) 273 | .unwrap(); 274 | let axis4: Action = action_set 275 | .create_action( 276 | &format!("{}_thumbstick_y", hand), 277 | &format!("{}_thumbstick_y", hand), 278 | &[], 279 | ) 280 | .unwrap(); 281 | vec![axis1, axis2, axis3, axis4] 282 | }; 283 | 284 | let use_alternate_input_source = supported_interaction_profiles 285 | .contains(&ext_string!(FB_HAND_TRACKING_AIM_EXTENSION_NAME)); 286 | 287 | Self { 288 | id, 289 | action_aim_pose, 290 | action_aim_space, 291 | action_grip_pose, 292 | action_grip_space, 293 | action_click, 294 | action_squeeze, 295 | handedness, 296 | click_state: ClickState::Done, 297 | squeeze_state: ClickState::Done, 298 | menu_gesture_sustain: 0, 299 | hand_tracker, 300 | action_buttons_common, 301 | action_axes_common, 302 | action_buttons_left, 303 | action_buttons_right, 304 | use_alternate_input_source, 305 | } 306 | } 307 | 308 | pub fn setup_inputs( 309 | instance: &Instance, 310 | session: &Session, 311 | needs_hands: bool, 312 | supported_interaction_profiles: Vec<&'static str>, 313 | ) -> (ActionSet, Self, Self) { 314 | let action_set = instance.create_action_set("hands", "Hands", 0).unwrap(); 315 | let right_hand = OpenXRInput::new( 316 | InputId(0), 317 | Handedness::Right, 318 | &action_set, 319 | &session, 320 | needs_hands, 321 | supported_interaction_profiles.clone(), 322 | ); 323 | let left_hand = OpenXRInput::new( 324 | InputId(1), 325 | Handedness::Left, 326 | &action_set, 327 | &session, 328 | needs_hands, 329 | supported_interaction_profiles.clone(), 330 | ); 331 | 332 | for profile in INTERACTION_PROFILES { 333 | if let Some(extension_name) = profile.required_extension { 334 | if !supported_interaction_profiles.contains(&ext_string!(extension_name)) { 335 | continue; 336 | } 337 | } 338 | 339 | if profile.path.is_empty() { 340 | continue; 341 | } 342 | 343 | let select = profile.standard_buttons[0]; 344 | let squeeze = Option::from(profile.standard_buttons[1]).filter(|&s| !s.is_empty()); 345 | let mut bindings = right_hand.get_bindings(instance, select, squeeze, &profile); 346 | bindings.extend( 347 | left_hand 348 | .get_bindings(instance, select, squeeze, &profile) 349 | .into_iter(), 350 | ); 351 | 352 | let path_controller = instance 353 | .string_to_path(profile.path) 354 | .expect(format!("Invalid interaction profile path: {}", profile.path).as_str()); 355 | if let Err(_) = 356 | instance.suggest_interaction_profile_bindings(path_controller, &bindings) 357 | { 358 | debug!( 359 | "Interaction profile path not available for this runtime: {:?}", 360 | profile.path 361 | ); 362 | } 363 | } 364 | 365 | session.attach_action_sets(&[&action_set]).unwrap(); 366 | 367 | (action_set, right_hand, left_hand) 368 | } 369 | 370 | fn get_bindings( 371 | &self, 372 | instance: &Instance, 373 | select_name: &str, 374 | squeeze_name: Option<&str>, 375 | interaction_profile: &InteractionProfile, 376 | ) -> Vec { 377 | let hand = hand_str(self.handedness); 378 | let path_aim_pose = instance 379 | .string_to_path(&format!("/user/hand/{}/input/aim/pose", hand)) 380 | .expect(&format!( 381 | "Failed to create path for /user/hand/{}/input/aim/pose", 382 | hand 383 | )); 384 | let binding_aim_pose = Binding::new(&self.action_aim_pose, path_aim_pose); 385 | let path_grip_pose = instance 386 | .string_to_path(&format!("/user/hand/{}/input/grip/pose", hand)) 387 | .expect(&format!( 388 | "Failed to create path for /user/hand/{}/input/grip/pose", 389 | hand 390 | )); 391 | let binding_grip_pose = Binding::new(&self.action_grip_pose, path_grip_pose); 392 | let path_click = instance 393 | .string_to_path(&format!("/user/hand/{}/input/{}", hand, select_name)) 394 | .expect(&format!( 395 | "Failed to create path for /user/hand/{}/input/{}", 396 | hand, select_name 397 | )); 398 | let binding_click = Binding::new(&self.action_click, path_click); 399 | 400 | let mut ret = vec![binding_aim_pose, binding_grip_pose, binding_click]; 401 | if let Some(squeeze_name) = squeeze_name { 402 | let path_squeeze = instance 403 | .string_to_path(&format!("/user/hand/{}/input/{}", hand, squeeze_name)) 404 | .expect(&format!( 405 | "Failed to create path for /user/hand/{}/input/{}", 406 | hand, squeeze_name 407 | )); 408 | let binding_squeeze = Binding::new(&self.action_squeeze, path_squeeze); 409 | ret.push(binding_squeeze); 410 | } 411 | 412 | bind_inputs!( 413 | self.action_buttons_common, 414 | interaction_profile.standard_buttons, 415 | hand, 416 | instance, 417 | ret 418 | ); 419 | 420 | if !interaction_profile.left_buttons.is_empty() && hand == "left" { 421 | bind_inputs!( 422 | self.action_buttons_left, 423 | interaction_profile.left_buttons, 424 | hand, 425 | instance, 426 | ret 427 | ); 428 | } else if !interaction_profile.right_buttons.is_empty() && hand == "right" { 429 | bind_inputs!( 430 | self.action_buttons_right, 431 | interaction_profile.right_buttons, 432 | hand, 433 | instance, 434 | ret 435 | ); 436 | } 437 | 438 | bind_inputs!( 439 | self.action_axes_common, 440 | interaction_profile.standard_axes, 441 | hand, 442 | instance, 443 | ret 444 | ); 445 | 446 | ret 447 | } 448 | 449 | pub fn frame( 450 | &mut self, 451 | session: &Session, 452 | frame_state: &FrameState, 453 | base_space: &Space, 454 | viewer: &RigidTransform3D, 455 | ) -> Frame { 456 | use euclid::Vector3D; 457 | let mut target_ray_origin = pose_for(&self.action_aim_space, frame_state, base_space); 458 | 459 | let grip_origin = pose_for(&self.action_grip_space, frame_state, base_space); 460 | 461 | let mut menu_selected = false; 462 | // Check if the palm is facing up. This is our "menu" gesture. 463 | if let Some(grip_origin) = grip_origin { 464 | // The X axis of the grip is perpendicular to the palm, however its 465 | // direction is the opposite for each hand 466 | // 467 | // We obtain a unit vector pointing out of the palm 468 | let x_dir = if let Handedness::Left = self.handedness { 469 | 1.0 470 | } else { 471 | -1.0 472 | }; 473 | // Rotate it by the grip to obtain the desired vector 474 | let grip_x = grip_origin 475 | .rotation 476 | .transform_vector3d(Vector3D::new(x_dir, 0.0, 0.0)); 477 | let gaze = viewer 478 | .rotation 479 | .transform_vector3d(Vector3D::new(0., 0., 1.)); 480 | 481 | // If the angle is close enough to 0, its cosine will be 482 | // close to 1 483 | // check if the user's gaze is parallel to the palm 484 | if gaze.dot(grip_x) > 0.95 { 485 | let input_relative = (viewer.translation - grip_origin.translation).normalize(); 486 | // if so, check if the user is actually looking at the palm 487 | if gaze.dot(input_relative) > 0.95 { 488 | self.menu_gesture_sustain += 1; 489 | if self.menu_gesture_sustain > MENU_GESTURE_SUSTAIN_THRESHOLD { 490 | menu_selected = true; 491 | self.menu_gesture_sustain = 0; 492 | } 493 | } else { 494 | self.menu_gesture_sustain = 0 495 | } 496 | } else { 497 | self.menu_gesture_sustain = 0; 498 | } 499 | } else { 500 | self.menu_gesture_sustain = 0; 501 | } 502 | 503 | let hand = hand_str(self.handedness); 504 | let click = self.action_click.state(session, Path::NULL).unwrap(); 505 | let squeeze = self.action_squeeze.state(session, Path::NULL).unwrap(); 506 | let (button_values, buttons_changed) = { 507 | let mut changed = false; 508 | let mut values = Vec::::new(); 509 | let mut sync_buttons = |actions: &Vec>| { 510 | let buttons = actions 511 | .iter() 512 | .map(|action| { 513 | let state = action.state(session, Path::NULL).unwrap(); 514 | changed = changed || state.changed_since_last_sync; 515 | state.current_state 516 | }) 517 | .collect::>(); 518 | values.extend_from_slice(&buttons); 519 | }; 520 | sync_buttons(&self.action_buttons_common); 521 | if hand == "left" { 522 | sync_buttons(&self.action_buttons_left); 523 | } else if hand == "right" { 524 | sync_buttons(&self.action_buttons_right); 525 | } 526 | (values, changed) 527 | }; 528 | 529 | let (axis_values, axes_changed) = { 530 | let mut changed = false; 531 | let values = self 532 | .action_axes_common 533 | .iter() 534 | .enumerate() 535 | .map(|(i, action)| { 536 | let state = action.state(session, Path::NULL).unwrap(); 537 | changed = changed || state.changed_since_last_sync; 538 | // Invert input from y axes 539 | state.current_state * if i % 2 == 1 { -1.0 } else { 1.0 } 540 | }) 541 | .collect::>(); 542 | (values, changed) 543 | }; 544 | 545 | let input_changed = buttons_changed || axes_changed; 546 | 547 | let (click_is_active, mut click_event) = if !self.use_alternate_input_source { 548 | self.click_state 549 | .update_from_action(&self.action_click, session, menu_selected) 550 | } else { 551 | (true, None) 552 | }; 553 | let (squeeze_is_active, squeeze_event) = 554 | self.squeeze_state 555 | .update_from_action(&self.action_squeeze, session, menu_selected); 556 | 557 | let mut aim_state: Option = None; 558 | let hand = self.hand_tracker.as_ref().and_then(|tracker| { 559 | locate_hand( 560 | base_space, 561 | tracker, 562 | frame_state, 563 | self.use_alternate_input_source, 564 | session, 565 | &mut aim_state, 566 | ) 567 | }); 568 | 569 | let mut pressed = click_is_active && click.current_state; 570 | let squeezed = squeeze_is_active && squeeze.current_state; 571 | 572 | if let Some(state) = aim_state { 573 | target_ray_origin.replace(super::transform(&state.aim_pose)); 574 | let index_pinching = state 575 | .status 576 | .intersects(HandTrackingAimFlagsFB::INDEX_PINCHING); 577 | click_event = self 578 | .click_state 579 | .update_from_value(index_pinching, true, menu_selected); 580 | pressed = index_pinching; 581 | } 582 | 583 | let input_frame = InputFrame { 584 | target_ray_origin, 585 | id: self.id, 586 | pressed, 587 | squeezed, 588 | grip_origin, 589 | hand, 590 | button_values, 591 | axis_values, 592 | input_changed, 593 | }; 594 | 595 | Frame { 596 | frame: input_frame, 597 | select: click_event, 598 | squeeze: squeeze_event, 599 | menu_selected, 600 | } 601 | } 602 | 603 | pub fn input_source(&self) -> InputSource { 604 | let hand_support = if self.hand_tracker.is_some() { 605 | // openxr runtimes must always support all or none joints 606 | Some(Hand::<()>::default().map(|_, _| Some(()))) 607 | } else { 608 | None 609 | }; 610 | InputSource { 611 | handedness: self.handedness, 612 | id: self.id, 613 | target_ray_mode: TargetRayMode::TrackedPointer, 614 | supports_grip: true, 615 | profiles: vec![], 616 | hand_support, 617 | } 618 | } 619 | } 620 | 621 | fn pose_for( 622 | action_space: &Space, 623 | frame_state: &FrameState, 624 | base_space: &Space, 625 | ) -> Option> { 626 | let location = action_space 627 | .locate(base_space, frame_state.predicted_display_time) 628 | .unwrap(); 629 | let pose_valid = location 630 | .location_flags 631 | .intersects(SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::ORIENTATION_VALID); 632 | if pose_valid { 633 | Some(super::transform(&location.pose)) 634 | } else { 635 | None 636 | } 637 | } 638 | 639 | fn locate_hand( 640 | base_space: &Space, 641 | tracker: &HandTracker, 642 | frame_state: &FrameState, 643 | use_alternate_input_source: bool, 644 | session: &Session, 645 | aim_state: &mut Option, 646 | ) -> Option>> { 647 | let mut state = HandTrackingAimStateFB::out(std::ptr::null_mut()); 648 | let locations = { 649 | if !use_alternate_input_source { 650 | base_space.locate_hand_joints(tracker, frame_state.predicted_display_time) 651 | } else { 652 | let locate_info = HandJointsLocateInfoEXT { 653 | ty: HandJointsLocateInfoEXT::TYPE, 654 | next: std::ptr::null(), 655 | base_space: base_space.as_raw(), 656 | time: frame_state.predicted_display_time, 657 | }; 658 | 659 | let mut locations = MaybeUninit::<[HandJointLocation; HAND_JOINT_COUNT]>::uninit(); 660 | let mut location_info = HandJointLocationsEXT { 661 | ty: HandJointLocationsEXT::TYPE, 662 | next: &mut state as *mut _ as *mut c_void, 663 | is_active: false.into(), 664 | joint_count: HAND_JOINT_COUNT as u32, 665 | joint_locations: locations.as_mut_ptr() as _, 666 | }; 667 | 668 | // Check if hand tracking is supported by the session instance 669 | let raw_hand_tracker = session.instance().exts().ext_hand_tracking.as_ref()?; 670 | 671 | unsafe { 672 | Ok( 673 | match (raw_hand_tracker.locate_hand_joints)( 674 | tracker.as_raw(), 675 | &locate_info, 676 | &mut location_info, 677 | ) { 678 | openxr::sys::Result::SUCCESS if location_info.is_active.into() => { 679 | aim_state.replace(state.assume_init()); 680 | Some(locations.assume_init()) 681 | } 682 | _ => None, 683 | }, 684 | ) 685 | } 686 | } 687 | }; 688 | let locations = if let Ok(Some(ref locations)) = locations { 689 | Hand { 690 | wrist: Some(&locations[HandJoint::WRIST]), 691 | thumb_metacarpal: Some(&locations[HandJoint::THUMB_METACARPAL]), 692 | thumb_phalanx_proximal: Some(&locations[HandJoint::THUMB_PROXIMAL]), 693 | thumb_phalanx_distal: Some(&locations[HandJoint::THUMB_DISTAL]), 694 | thumb_phalanx_tip: Some(&locations[HandJoint::THUMB_TIP]), 695 | index: Finger { 696 | metacarpal: Some(&locations[HandJoint::INDEX_METACARPAL]), 697 | phalanx_proximal: Some(&locations[HandJoint::INDEX_PROXIMAL]), 698 | phalanx_intermediate: Some(&locations[HandJoint::INDEX_INTERMEDIATE]), 699 | phalanx_distal: Some(&locations[HandJoint::INDEX_DISTAL]), 700 | phalanx_tip: Some(&locations[HandJoint::INDEX_TIP]), 701 | }, 702 | middle: Finger { 703 | metacarpal: Some(&locations[HandJoint::MIDDLE_METACARPAL]), 704 | phalanx_proximal: Some(&locations[HandJoint::MIDDLE_PROXIMAL]), 705 | phalanx_intermediate: Some(&locations[HandJoint::MIDDLE_INTERMEDIATE]), 706 | phalanx_distal: Some(&locations[HandJoint::MIDDLE_DISTAL]), 707 | phalanx_tip: Some(&locations[HandJoint::MIDDLE_TIP]), 708 | }, 709 | ring: Finger { 710 | metacarpal: Some(&locations[HandJoint::RING_METACARPAL]), 711 | phalanx_proximal: Some(&locations[HandJoint::RING_PROXIMAL]), 712 | phalanx_intermediate: Some(&locations[HandJoint::RING_INTERMEDIATE]), 713 | phalanx_distal: Some(&locations[HandJoint::RING_DISTAL]), 714 | phalanx_tip: Some(&locations[HandJoint::RING_TIP]), 715 | }, 716 | little: Finger { 717 | metacarpal: Some(&locations[HandJoint::LITTLE_METACARPAL]), 718 | phalanx_proximal: Some(&locations[HandJoint::LITTLE_PROXIMAL]), 719 | phalanx_intermediate: Some(&locations[HandJoint::LITTLE_INTERMEDIATE]), 720 | phalanx_distal: Some(&locations[HandJoint::LITTLE_DISTAL]), 721 | phalanx_tip: Some(&locations[HandJoint::LITTLE_TIP]), 722 | }, 723 | } 724 | } else { 725 | return None; 726 | }; 727 | 728 | Some(Box::new(locations.map(|loc, _| { 729 | loc.and_then(|location| { 730 | let pose_valid = location.location_flags.intersects( 731 | SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::ORIENTATION_VALID, 732 | ); 733 | if pose_valid { 734 | Some(JointFrame { 735 | pose: super::transform(&location.pose), 736 | radius: location.radius, 737 | }) 738 | } else { 739 | None 740 | } 741 | }) 742 | }))) 743 | } 744 | -------------------------------------------------------------------------------- /webxr/openxr/interaction_profiles.rs: -------------------------------------------------------------------------------- 1 | use openxr::{ 2 | sys::{ 3 | BD_CONTROLLER_INTERACTION_EXTENSION_NAME, EXT_HAND_INTERACTION_EXTENSION_NAME, 4 | EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME, 5 | EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME, FB_HAND_TRACKING_AIM_EXTENSION_NAME, 6 | FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME, 7 | HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME, 8 | HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME, 9 | META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME, ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME, 10 | }, 11 | ExtensionSet, 12 | }; 13 | 14 | #[macro_export] 15 | macro_rules! ext_string { 16 | ($ext_name:expr) => { 17 | std::str::from_utf8($ext_name).unwrap() 18 | }; 19 | } 20 | 21 | #[derive(Clone, Copy, Debug, PartialEq)] 22 | pub enum InteractionProfileType { 23 | KhrSimpleController, 24 | BytedancePicoNeo3Controller, 25 | BytedancePico4Controller, 26 | BytedancePicoG3Controller, 27 | GoogleDaydreamController, 28 | HpMixedRealityController, 29 | HtcViveController, 30 | HtcViveCosmosController, 31 | HtcViveFocus3Controller, 32 | MagicLeap2Controller, 33 | MicrosoftMixedRealityMotionController, 34 | OculusGoController, 35 | OculusTouchController, 36 | FacebookTouchControllerPro, 37 | MetaTouchPlusController, 38 | MetaTouchControllerRiftCv1, 39 | MetaTouchControllerQuest1RiftS, 40 | MetaTouchControllerQuest2, 41 | SamsungOdysseyController, 42 | ValveIndexController, 43 | ExtHandInteraction, 44 | FbHandTrackingAim, 45 | } 46 | 47 | #[derive(Clone, Copy, Debug)] 48 | pub struct InteractionProfile<'a> { 49 | pub profile_type: InteractionProfileType, 50 | /// The interaction profile path 51 | pub path: &'static str, 52 | /// The OpenXR extension, if any, required to use this profile 53 | pub required_extension: Option<&'a [u8]>, 54 | /// Trigger, Grip, Touchpad, Thumbstick 55 | pub standard_buttons: &'a [&'a str], 56 | /// Touchpad X, Touchpad Y, Thumbstick X, Thumbstick Y 57 | pub standard_axes: &'a [&'a str], 58 | /// Any additional buttons on the left controller 59 | pub left_buttons: &'a [&'a str], 60 | /// Any additional buttons on the right controller 61 | pub right_buttons: &'a [&'a str], 62 | /// The corresponding WebXR Input Profile names 63 | pub profiles: &'a [&'a str], 64 | } 65 | 66 | pub static KHR_SIMPLE_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 67 | profile_type: InteractionProfileType::KhrSimpleController, 68 | path: "/interaction_profiles/khr/simple_controller", 69 | required_extension: None, 70 | standard_buttons: &["select/click", "", "", ""], 71 | standard_axes: &["", "", "", ""], 72 | left_buttons: &[], 73 | right_buttons: &[], 74 | profiles: &["generic-trigger"], 75 | }; 76 | 77 | pub static BYTEDANCE_PICO_NEO3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 78 | profile_type: InteractionProfileType::BytedancePicoNeo3Controller, 79 | path: "/interaction_profiles/bytedance/pico_neo3_controller", 80 | required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME), 81 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 82 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 83 | left_buttons: &["x/click", "y/click"], 84 | right_buttons: &["a/click", "b/click"], 85 | profiles: &["pico-neo3", "generic-trigger-squeeze-thumbstick"], 86 | }; 87 | 88 | pub static BYTEDANCE_PICO_4_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 89 | profile_type: InteractionProfileType::BytedancePico4Controller, 90 | path: "/interaction_profiles/bytedance/pico4_controller", 91 | required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME), 92 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 93 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 94 | left_buttons: &["x/click", "y/click"], 95 | right_buttons: &["a/click", "b/click"], 96 | profiles: &["pico-4", "generic-trigger-squeeze-thumbstick"], 97 | }; 98 | 99 | pub static BYTEDANCE_PICO_G3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 100 | profile_type: InteractionProfileType::BytedancePicoG3Controller, 101 | path: "/interaction_profiles/bytedance/pico_g3_controller", 102 | required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME), 103 | standard_buttons: &["trigger/value", "", "", "thumbstick/click"], 104 | // Note: X/Y components not listed in the OpenXR spec currently due to vendor error. 105 | // See 106 | // It also uses the thumbstick path despite clearly being a touchpad, so 107 | // move those values into the touchpad axes slots 108 | standard_axes: &["thumbstick/x", "thumbstick/y", "", ""], 109 | left_buttons: &[], 110 | right_buttons: &[], 111 | // Note: There is no corresponding WebXR Input profile for the Pico G3, 112 | // but the controller seems identical to the G2, so use that instead. 113 | profiles: &["pico-g2", "generic-trigger-touchpad"], 114 | }; 115 | 116 | pub static GOOGLE_DAYDREAM_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 117 | profile_type: InteractionProfileType::GoogleDaydreamController, 118 | path: "/interaction_profiles/google/daydream_controller", 119 | required_extension: None, 120 | standard_buttons: &["select/click", "", "trackpad/click", ""], 121 | standard_axes: &["trackpad/x", "trackpad/y", "", ""], 122 | left_buttons: &[], 123 | right_buttons: &[], 124 | profiles: &["google-daydream", "generic-touchpad"], 125 | }; 126 | 127 | pub static HP_MIXED_REALITY_MOTION_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 128 | profile_type: InteractionProfileType::HpMixedRealityController, 129 | path: "/interaction_profiles/hp/mixed_reality_controller", 130 | required_extension: Some(EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME), 131 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 132 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 133 | left_buttons: &["x/click", "y/click"], 134 | right_buttons: &["a/click", "b/click"], 135 | profiles: &[ 136 | "hp-mixed-reality", 137 | "oculus-touch", 138 | "generic-trigger-squeeze-thumbstick", 139 | ], 140 | }; 141 | 142 | pub static HTC_VIVE_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 143 | profile_type: InteractionProfileType::HtcViveController, 144 | path: "/interaction_profiles/htc/vive_controller", 145 | required_extension: None, 146 | standard_buttons: &["trigger/value", "squeeze/click", "trackpad/click", ""], 147 | standard_axes: &["trackpad/x", "trackpad/y", "", ""], 148 | left_buttons: &[], 149 | right_buttons: &[], 150 | profiles: &["htc-vive", "generic-trigger-squeeze-touchpad"], 151 | }; 152 | 153 | pub static HTC_VIVE_COSMOS_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 154 | profile_type: InteractionProfileType::HtcViveCosmosController, 155 | path: "/interaction_profiles/htc/vive_cosmos_controller", 156 | required_extension: Some(HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME), 157 | standard_buttons: &["trigger/value", "squeeze/click", "", "thumbstick/click"], 158 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 159 | left_buttons: &["x/click", "y/click"], 160 | right_buttons: &["a/click", "b/click"], 161 | profiles: &["htc-vive-cosmos", "generic-trigger-squeeze-thumbstick"], 162 | }; 163 | 164 | pub static HTC_VIVE_FOCUS3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 165 | profile_type: InteractionProfileType::HtcViveFocus3Controller, 166 | path: "/interaction_profiles/htc/vive_focus3_controller", 167 | required_extension: Some(HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME), 168 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 169 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 170 | left_buttons: &["x/click", "y/click"], 171 | right_buttons: &["a/click", "b/click"], 172 | profiles: &["htc-vive-focus-3", "generic-trigger-squeeze-thumbstick"], 173 | }; 174 | 175 | pub static MAGIC_LEAP_2_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 176 | profile_type: InteractionProfileType::MagicLeap2Controller, 177 | path: "/interaction_profiles/ml/ml2_controller", 178 | required_extension: Some(ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME), 179 | standard_buttons: &["trigger/value", "", "trackpad/click", ""], 180 | standard_axes: &["trackpad/x", "trackpad/y", "", ""], 181 | left_buttons: &[], 182 | right_buttons: &[], 183 | // Note: There is no corresponding WebXR Input profile for the Magic Leap 2, 184 | // but the controller seems mostly identical to the 1, so use that instead. 185 | profiles: &["magicleap-one", "generic-trigger-squeeze-touchpad"], 186 | }; 187 | 188 | pub static MICROSOFT_MIXED_REALITY_MOTION_CONTROLLER_PROFILE: InteractionProfile = 189 | InteractionProfile { 190 | profile_type: InteractionProfileType::MicrosoftMixedRealityMotionController, 191 | path: "/interaction_profiles/microsoft/motion_controller", 192 | required_extension: None, 193 | standard_buttons: &[ 194 | "trigger/value", 195 | "squeeze/click", 196 | "trackpad/click", 197 | "thumbstick/click", 198 | ], 199 | standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"], 200 | left_buttons: &[], 201 | right_buttons: &[], 202 | profiles: &[ 203 | "microsoft-mixed-reality", 204 | "generic-trigger-squeeze-touchpad-thumbstick", 205 | ], 206 | }; 207 | 208 | pub static OCULUS_GO_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 209 | profile_type: InteractionProfileType::OculusGoController, 210 | path: "/interaction_profiles/oculus/go_controller", 211 | required_extension: None, 212 | standard_buttons: &["trigger/click", "", "trackpad/click", ""], 213 | standard_axes: &["trackpad/x", "trackpad/y", "", ""], 214 | left_buttons: &[], 215 | right_buttons: &[], 216 | profiles: &["oculus-go", "generic-trigger-touchpad"], 217 | }; 218 | 219 | pub static OCULUS_TOUCH_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 220 | profile_type: InteractionProfileType::OculusTouchController, 221 | path: "/interaction_profiles/oculus/touch_controller", 222 | required_extension: None, 223 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 224 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 225 | left_buttons: &["x/click", "y/click"], 226 | right_buttons: &["a/click", "b/click"], 227 | profiles: &[ 228 | "oculus-touch-v3", 229 | "oculus-touch-v2", 230 | "oculus-touch", 231 | "generic-trigger-squeeze-thumbstick", 232 | ], 233 | }; 234 | 235 | pub static FACEBOOK_TOUCH_CONTROLLER_PRO_PROFILE: InteractionProfile = InteractionProfile { 236 | profile_type: InteractionProfileType::FacebookTouchControllerPro, 237 | path: "/interaction_profiles/facebook/touch_controller_pro", 238 | required_extension: Some(FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME), 239 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 240 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 241 | left_buttons: &["x/click", "y/click"], 242 | right_buttons: &["a/click", "b/click"], 243 | profiles: &[ 244 | "meta-quest-touch-pro", 245 | "oculus-touch-v2", 246 | "oculus-touch", 247 | "generic-trigger-squeeze-thumbstick", 248 | ], 249 | }; 250 | 251 | pub static META_TOUCH_CONTROLLER_PLUS_PROFILE: InteractionProfile = InteractionProfile { 252 | profile_type: InteractionProfileType::MetaTouchPlusController, 253 | path: "/interaction_profiles/meta/touch_controller_plus", 254 | required_extension: Some(META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME), 255 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 256 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 257 | left_buttons: &["x/click", "y/click"], 258 | right_buttons: &["a/click", "b/click"], 259 | profiles: &[ 260 | "meta-quest-touch-plus", 261 | "oculus-touch-v3", 262 | "oculus-touch", 263 | "generic-trigger-squeeze-thumbstick", 264 | ], 265 | }; 266 | 267 | pub static META_TOUCH_CONTROLLER_RIFT_CV1_PROFILE: InteractionProfile = InteractionProfile { 268 | profile_type: InteractionProfileType::MetaTouchControllerRiftCv1, 269 | path: "/interaction_profiles/meta/touch_controller_rift_cv1", 270 | required_extension: None, 271 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 272 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 273 | left_buttons: &["x/click", "y/click"], 274 | right_buttons: &["a/click", "b/click"], 275 | profiles: &["oculus-touch", "generic-trigger-squeeze-thumbstick"], 276 | }; 277 | 278 | pub static META_TOUCH_CONTROLLER_QUEST_1_RIFT_S_PROFILE: InteractionProfile = InteractionProfile { 279 | profile_type: InteractionProfileType::MetaTouchControllerQuest1RiftS, 280 | path: "/interaction_profiles/meta/touch_controller_quest_1_rift_s", 281 | required_extension: None, 282 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 283 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 284 | left_buttons: &["x/click", "y/click"], 285 | right_buttons: &["a/click", "b/click"], 286 | profiles: &[ 287 | "oculus-touch-v2", 288 | "oculus-touch", 289 | "generic-trigger-squeeze-thumbstick", 290 | ], 291 | }; 292 | 293 | pub static META_TOUCH_CONTROLLER_QUEST_2_PROFILE: InteractionProfile = InteractionProfile { 294 | profile_type: InteractionProfileType::MetaTouchControllerQuest2, 295 | path: "/interaction_profiles/meta/touch_controller_quest_2", 296 | required_extension: None, 297 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 298 | standard_axes: &["", "", "thumbstick/x", "thumbstick/y"], 299 | left_buttons: &["x/click", "y/click"], 300 | right_buttons: &["a/click", "b/click"], 301 | profiles: &[ 302 | "oculus-touch-v3", 303 | "oculus-touch-v2", 304 | "oculus-touch", 305 | "generic-trigger-squeeze-thumbstick", 306 | ], 307 | }; 308 | 309 | pub static SAMSUNG_ODYSSEY_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 310 | profile_type: InteractionProfileType::SamsungOdysseyController, 311 | path: "/interaction_profiles/samsung/odyssey_controller", 312 | required_extension: Some(EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME), 313 | standard_buttons: &[ 314 | "trigger/value", 315 | "squeeze/click", 316 | "trackpad/click", 317 | "thumbstick/click", 318 | ], 319 | standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"], 320 | left_buttons: &[], 321 | right_buttons: &[], 322 | profiles: &[ 323 | "samsung-odyssey", 324 | "microsoft-mixed-reality", 325 | "generic-trigger-squeeze-touchpad-thumbstick", 326 | ], 327 | }; 328 | 329 | pub static VALVE_INDEX_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile { 330 | profile_type: InteractionProfileType::ValveIndexController, 331 | path: "/interaction_profiles/valve/index_controller", 332 | required_extension: None, 333 | standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"], 334 | standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"], 335 | left_buttons: &["a/click", "b/click"], 336 | right_buttons: &["a/click", "b/click"], 337 | profiles: &["valve-index", "generic-trigger-squeeze-touchpad-thumbstick"], 338 | }; 339 | 340 | pub static EXT_HAND_INTERACTION_PROFILE: InteractionProfile = InteractionProfile { 341 | profile_type: InteractionProfileType::ExtHandInteraction, 342 | path: "/interaction_profiles/ext/hand_interaction_ext", 343 | required_extension: Some(EXT_HAND_INTERACTION_EXTENSION_NAME), 344 | standard_buttons: &["pinch_ext/value", "", "", ""], 345 | standard_axes: &["", "", "", ""], 346 | left_buttons: &[], 347 | right_buttons: &[], 348 | profiles: &["generic-hand-select", "generic-hand"], 349 | }; 350 | 351 | pub static FB_HAND_TRACKING_AIM_PROFILE: InteractionProfile = InteractionProfile { 352 | profile_type: InteractionProfileType::FbHandTrackingAim, 353 | path: "", 354 | required_extension: Some(FB_HAND_TRACKING_AIM_EXTENSION_NAME), 355 | standard_buttons: &["", "", "", ""], 356 | standard_axes: &["", "", "", ""], 357 | left_buttons: &[], 358 | right_buttons: &[], 359 | profiles: &["generic-hand-select", "generic-hand"], 360 | }; 361 | 362 | pub static INTERACTION_PROFILES: [InteractionProfile; 22] = [ 363 | KHR_SIMPLE_CONTROLLER_PROFILE, 364 | BYTEDANCE_PICO_NEO3_CONTROLLER_PROFILE, 365 | BYTEDANCE_PICO_4_CONTROLLER_PROFILE, 366 | BYTEDANCE_PICO_G3_CONTROLLER_PROFILE, 367 | GOOGLE_DAYDREAM_CONTROLLER_PROFILE, 368 | HP_MIXED_REALITY_MOTION_CONTROLLER_PROFILE, 369 | HTC_VIVE_CONTROLLER_PROFILE, 370 | HTC_VIVE_COSMOS_CONTROLLER_PROFILE, 371 | HTC_VIVE_FOCUS3_CONTROLLER_PROFILE, 372 | MAGIC_LEAP_2_CONTROLLER_PROFILE, 373 | MICROSOFT_MIXED_REALITY_MOTION_CONTROLLER_PROFILE, 374 | OCULUS_GO_CONTROLLER_PROFILE, 375 | OCULUS_TOUCH_CONTROLLER_PROFILE, 376 | FACEBOOK_TOUCH_CONTROLLER_PRO_PROFILE, 377 | META_TOUCH_CONTROLLER_PLUS_PROFILE, 378 | META_TOUCH_CONTROLLER_RIFT_CV1_PROFILE, 379 | META_TOUCH_CONTROLLER_QUEST_1_RIFT_S_PROFILE, 380 | META_TOUCH_CONTROLLER_QUEST_2_PROFILE, 381 | SAMSUNG_ODYSSEY_CONTROLLER_PROFILE, 382 | VALVE_INDEX_CONTROLLER_PROFILE, 383 | EXT_HAND_INTERACTION_PROFILE, 384 | FB_HAND_TRACKING_AIM_PROFILE, 385 | ]; 386 | 387 | pub fn get_profiles_from_path(path: String) -> &'static [&'static str] { 388 | INTERACTION_PROFILES 389 | .iter() 390 | .find(|profile| profile.path == path) 391 | .map_or(&[], |profile| profile.profiles) 392 | } 393 | 394 | pub fn get_supported_interaction_profiles( 395 | supported_extensions: &ExtensionSet, 396 | enabled_extensions: &mut ExtensionSet, 397 | ) -> Vec<&'static str> { 398 | let mut extensions = Vec::new(); 399 | if supported_extensions.bd_controller_interaction { 400 | extensions.push(ext_string!(BD_CONTROLLER_INTERACTION_EXTENSION_NAME)); 401 | enabled_extensions.bd_controller_interaction = true; 402 | } 403 | if supported_extensions.ext_hp_mixed_reality_controller { 404 | extensions.push(ext_string!(EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME)); 405 | enabled_extensions.ext_hp_mixed_reality_controller = true; 406 | } 407 | if supported_extensions.ext_samsung_odyssey_controller { 408 | extensions.push(ext_string!(EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME)); 409 | enabled_extensions.ext_samsung_odyssey_controller = true; 410 | } 411 | if supported_extensions.ml_ml2_controller_interaction { 412 | extensions.push(ext_string!(ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME)); 413 | enabled_extensions.ml_ml2_controller_interaction = true; 414 | } 415 | if supported_extensions.htc_vive_cosmos_controller_interaction { 416 | extensions.push(ext_string!( 417 | HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME 418 | )); 419 | enabled_extensions.htc_vive_cosmos_controller_interaction = true; 420 | } 421 | if supported_extensions.htc_vive_focus3_controller_interaction { 422 | extensions.push(ext_string!( 423 | HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME 424 | )); 425 | enabled_extensions.htc_vive_focus3_controller_interaction = true; 426 | } 427 | if supported_extensions.fb_touch_controller_pro { 428 | extensions.push(ext_string!(FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME)); 429 | enabled_extensions.fb_touch_controller_pro = true; 430 | } 431 | if supported_extensions.meta_touch_controller_plus { 432 | extensions.push(ext_string!(META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME)); 433 | enabled_extensions.meta_touch_controller_plus = true; 434 | } 435 | if supported_extensions.ext_hand_interaction { 436 | extensions.push(ext_string!(EXT_HAND_INTERACTION_EXTENSION_NAME)); 437 | enabled_extensions.ext_hand_interaction = true; 438 | } 439 | if supported_extensions.fb_hand_tracking_aim { 440 | extensions.push(ext_string!(FB_HAND_TRACKING_AIM_EXTENSION_NAME)); 441 | enabled_extensions.fb_hand_tracking_aim = true; 442 | } 443 | extensions 444 | } 445 | -------------------------------------------------------------------------------- /webxr/surfman_layer_manager.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! An implementation of layer management using surfman 6 | 7 | use crate::gl_utils::GlClearer; 8 | use euclid::{Point2D, Rect, Size2D}; 9 | use glow::{self as gl, Context as Gl, HasContext, PixelUnpackData}; 10 | use std::collections::HashMap; 11 | use std::num::NonZeroU32; 12 | use surfman::chains::{PreserveBuffer, SwapChains, SwapChainsAPI}; 13 | use surfman::{Context as SurfmanContext, Device as SurfmanDevice, SurfaceAccess, SurfaceTexture}; 14 | use webxr_api::{ 15 | ContextId, Error, GLContexts, GLTypes, LayerId, LayerInit, LayerManagerAPI, SubImage, 16 | SubImages, Viewports, 17 | }; 18 | 19 | #[derive(Copy, Clone, Debug)] 20 | pub enum SurfmanGL {} 21 | 22 | impl GLTypes for SurfmanGL { 23 | type Device = SurfmanDevice; 24 | type Context = SurfmanContext; 25 | type Bindings = Gl; 26 | } 27 | 28 | pub struct SurfmanLayerManager { 29 | layers: Vec<(ContextId, LayerId)>, 30 | swap_chains: SwapChains, 31 | surface_textures: HashMap, 32 | depth_stencil_textures: HashMap>, 33 | viewports: Viewports, 34 | clearer: GlClearer, 35 | } 36 | 37 | impl SurfmanLayerManager { 38 | pub fn new( 39 | viewports: Viewports, 40 | swap_chains: SwapChains, 41 | ) -> SurfmanLayerManager { 42 | let layers = Vec::new(); 43 | let surface_textures = HashMap::new(); 44 | let depth_stencil_textures = HashMap::new(); 45 | let clearer = GlClearer::new(false); 46 | SurfmanLayerManager { 47 | layers, 48 | swap_chains, 49 | surface_textures, 50 | depth_stencil_textures, 51 | viewports, 52 | clearer, 53 | } 54 | } 55 | } 56 | 57 | impl LayerManagerAPI for SurfmanLayerManager { 58 | fn create_layer( 59 | &mut self, 60 | device: &mut SurfmanDevice, 61 | contexts: &mut dyn GLContexts, 62 | context_id: ContextId, 63 | init: LayerInit, 64 | ) -> Result { 65 | let texture_size = init.texture_size(&self.viewports); 66 | let layer_id = LayerId::new(); 67 | let access = SurfaceAccess::GPUOnly; 68 | let size = texture_size.to_untyped(); 69 | // TODO: Treat depth and stencil separately? 70 | let has_depth_stencil = match init { 71 | LayerInit::WebGLLayer { stencil, depth, .. } => stencil | depth, 72 | LayerInit::ProjectionLayer { stencil, depth, .. } => stencil | depth, 73 | }; 74 | if has_depth_stencil { 75 | let gl = contexts 76 | .bindings(device, context_id) 77 | .ok_or(Error::NoMatchingDevice)?; 78 | let depth_stencil_texture = unsafe { gl.create_texture().ok() }; 79 | unsafe { 80 | gl.bind_texture(gl::TEXTURE_2D, depth_stencil_texture); 81 | gl.tex_image_2d( 82 | gl::TEXTURE_2D, 83 | 0, 84 | gl::DEPTH24_STENCIL8 as _, 85 | size.width, 86 | size.height, 87 | 0, 88 | gl::DEPTH_STENCIL, 89 | gl::UNSIGNED_INT_24_8, 90 | PixelUnpackData::Slice(None), 91 | ); 92 | } 93 | self.depth_stencil_textures 94 | .insert(layer_id, depth_stencil_texture); 95 | } 96 | let context = contexts 97 | .context(device, context_id) 98 | .ok_or(Error::NoMatchingDevice)?; 99 | self.swap_chains 100 | .create_detached_swap_chain(layer_id, size, device, context, access) 101 | .map_err(|err| Error::BackendSpecific(format!("{:?}", err)))?; 102 | self.layers.push((context_id, layer_id)); 103 | Ok(layer_id) 104 | } 105 | 106 | fn destroy_layer( 107 | &mut self, 108 | device: &mut SurfmanDevice, 109 | contexts: &mut dyn GLContexts, 110 | context_id: ContextId, 111 | layer_id: LayerId, 112 | ) { 113 | self.clearer 114 | .destroy_layer(device, contexts, context_id, layer_id); 115 | let context = match contexts.context(device, context_id) { 116 | Some(context) => context, 117 | None => return, 118 | }; 119 | self.layers.retain(|&ids| ids != (context_id, layer_id)); 120 | let _ = self.swap_chains.destroy(layer_id, device, context); 121 | self.surface_textures.remove(&layer_id); 122 | if let Some(depth_stencil_texture) = self.depth_stencil_textures.remove(&layer_id) { 123 | let gl = contexts.bindings(device, context_id).unwrap(); 124 | if let Some(depth_stencil_texture) = depth_stencil_texture { 125 | unsafe { 126 | gl.delete_texture(depth_stencil_texture); 127 | } 128 | } 129 | } 130 | } 131 | 132 | fn layers(&self) -> &[(ContextId, LayerId)] { 133 | &self.layers[..] 134 | } 135 | 136 | fn begin_frame( 137 | &mut self, 138 | device: &mut SurfmanDevice, 139 | contexts: &mut dyn GLContexts, 140 | layers: &[(ContextId, LayerId)], 141 | ) -> Result, Error> { 142 | layers 143 | .iter() 144 | .map(|&(context_id, layer_id)| { 145 | let context = contexts 146 | .context(device, context_id) 147 | .ok_or(Error::NoMatchingDevice)?; 148 | let swap_chain = self 149 | .swap_chains 150 | .get(layer_id) 151 | .ok_or(Error::NoMatchingDevice)?; 152 | let surface_size = Size2D::from_untyped(swap_chain.size()); 153 | let surface_texture = swap_chain 154 | .take_surface_texture(device, context) 155 | .map_err(|_| Error::NoMatchingDevice)?; 156 | let color_texture = device.surface_texture_object(&surface_texture); 157 | let color_target = device.surface_gl_texture_target(); 158 | let depth_stencil_texture = self 159 | .depth_stencil_textures 160 | .get(&layer_id) 161 | .cloned() 162 | .flatten(); 163 | let texture_array_index = None; 164 | let origin = Point2D::new(0, 0); 165 | let sub_image = Some(SubImage { 166 | color_texture, 167 | depth_stencil_texture: depth_stencil_texture.map(|nt| nt.0.get()), 168 | texture_array_index, 169 | viewport: Rect::new(origin, surface_size), 170 | }); 171 | let view_sub_images = self 172 | .viewports 173 | .viewports 174 | .iter() 175 | .map(|&viewport| SubImage { 176 | color_texture, 177 | depth_stencil_texture: depth_stencil_texture.map(|texture| texture.0.get()), 178 | texture_array_index, 179 | viewport, 180 | }) 181 | .collect(); 182 | self.surface_textures.insert(layer_id, surface_texture); 183 | self.clearer.clear( 184 | device, 185 | contexts, 186 | context_id, 187 | layer_id, 188 | NonZeroU32::new(color_texture).map(gl::NativeTexture), 189 | color_target, 190 | depth_stencil_texture, 191 | ); 192 | Ok(SubImages { 193 | layer_id, 194 | sub_image, 195 | view_sub_images, 196 | }) 197 | }) 198 | .collect() 199 | } 200 | 201 | fn end_frame( 202 | &mut self, 203 | device: &mut SurfmanDevice, 204 | contexts: &mut dyn GLContexts, 205 | layers: &[(ContextId, LayerId)], 206 | ) -> Result<(), Error> { 207 | for &(context_id, layer_id) in layers { 208 | let gl = contexts 209 | .bindings(device, context_id) 210 | .ok_or(Error::NoMatchingDevice)?; 211 | unsafe { 212 | gl.flush(); 213 | } 214 | let context = contexts 215 | .context(device, context_id) 216 | .ok_or(Error::NoMatchingDevice)?; 217 | let surface_texture = self 218 | .surface_textures 219 | .remove(&layer_id) 220 | .ok_or(Error::NoMatchingDevice)?; 221 | let swap_chain = self 222 | .swap_chains 223 | .get(layer_id) 224 | .ok_or(Error::NoMatchingDevice)?; 225 | swap_chain 226 | .recycle_surface_texture(device, context, surface_texture) 227 | .map_err(|err| Error::BackendSpecific(format!("{:?}", err)))?; 228 | swap_chain 229 | .swap_buffers(device, context, PreserveBuffer::No) 230 | .map_err(|err| Error::BackendSpecific(format!("{:?}", err)))?; 231 | } 232 | Ok(()) 233 | } 234 | } 235 | --------------------------------------------------------------------------------