diff --git a/.gitignore b/.gitignore index fdd7db61..f8a4ca7e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,12 @@ sponza* # Ignore compiled shaders *.spv + +# ..except for the editor's, which are special and magic +!hotham-editor/src/shaders/*.spv + +# Checking in a socket is probably a terrible idea +*.socket + +# Ignore vscode settings +.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b15158c1..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", - "[glsl]": { - "editor.formatOnSave": false - }, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "webgl-glsl-editor.format.placeSpaceAfterKeywords": true -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a96d5f66..3f475813 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -98,8 +98,17 @@ "label": "Run Hotham simple scene on the simulator", "group": { "kind": "test", - "isDefault": true, + "isDefault": false, } }, + { + "label": "Test OpenXR Client", + "type": "shell", + "command": "./hotham-openxr-client/test.ps1", + "group": { + "kind": "test", + "isDefault": true, + } + } ] } diff --git a/Cargo.toml b/Cargo.toml index 39d54f14..8c3c9d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ members = [ "hotham-asset-client", "hotham-asset-server", "hotham-simulator", + "hotham-editor", + "hotham-editor-protocol", + "hotham-openxr-client", "hotham", ] diff --git a/examples/simple-scene/Cargo.toml b/examples/simple-scene/Cargo.toml index 61c36ad7..a20ee567 100644 --- a/examples/simple-scene/Cargo.toml +++ b/examples/simple-scene/Cargo.toml @@ -12,7 +12,17 @@ name = "hotham_simple_scene_example" path = "src/main.rs" [dependencies] -hotham = {path = "../../hotham"} +env_logger = "0.10.0" +hotham = {path = "../../hotham", features = ["editor"]} +hotham-editor-protocol = {path = "../../hotham-editor-protocol", optional = true} +log = "0.4.17" + +[features] +default = ["editor"] +editor = ["dep:uds_windows", "dep:hotham-editor-protocol"] + +[target.'cfg(windows)'.dependencies] +uds_windows = {version = "1.0.2", optional = true} [target.'cfg(target_os = "android")'.dependencies] ndk-glue = "0.6" @@ -76,5 +86,5 @@ version = 1 # # !! IMPORTANT !! [package.metadata.android.signing.release] -path = "../hotham_examples.keystore" keystore_password = "chomsky-vigilant-spa" +path = "../hotham_examples.keystore" diff --git a/examples/simple-scene/src/lib.rs b/examples/simple-scene/src/lib.rs index 16e94159..dddbf18e 100644 --- a/examples/simple-scene/src/lib.rs +++ b/examples/simple-scene/src/lib.rs @@ -1,6 +1,9 @@ use hotham::{ asset_importer::{self, add_model_to_world}, - components::{hand::Handedness, physics::SharedShape, Collider, LocalTransform, RigidBody}, + components::{ + hand::Handedness, physics::SharedShape, Collider, GlobalTransform, Info, LocalTransform, + Mesh, + }, hecs::World, systems::{ animation_system, debug::debug_system, grabbing_system, hands::add_hand, hands_system, @@ -9,6 +12,8 @@ use hotham::{ }, xr, Engine, HothamResult, TickData, }; +use hotham_editor_protocol::scene::{EditorEntity, EditorUpdates, Transform}; +use log::{debug, info}; #[derive(Clone, Debug, Default)] /// Most Hotham applications will want to keep track of some sort of state. @@ -24,11 +29,35 @@ pub fn main() { } pub fn real_main() -> HothamResult<()> { + env_logger::builder() + .filter_module("hotham-openxr-client", log::LevelFilter::Trace) + .filter_module("simple_scene_example", log::LevelFilter::Trace) + .init(); + + info!("Initialising Simple Scene example.."); + + #[cfg(feature = "editor")] + let mut editor = { + use hotham_editor_protocol::EditorClient; + use uds_windows::UnixStream; + info!("Connecting to editor.."); + let stream = UnixStream::connect("hotham_editor.socket")?; + EditorClient::new(stream) + }; + + info!("Building engine.."); let mut engine = Engine::new(); + info!("..done!"); + + info!("Initialising app.."); let mut state = Default::default(); init(&mut engine)?; + info!("Done! Entering main loop.."); while let Ok(tick_data) = engine.update() { + #[cfg(feature = "editor")] + sync_with_editor(&mut engine.world, &mut editor)?; + tick(tick_data, &mut engine, &mut state); engine.finish()?; } @@ -36,6 +65,48 @@ pub fn real_main() -> HothamResult<()> { Ok(()) } +fn sync_with_editor( + world: &mut World, + editor: &mut hotham_editor_protocol::EditorClient, +) -> HothamResult<()> { + use hotham::hecs::Entity; + let entities = world + .query_mut::<(&GlobalTransform, &Info)>() + .with::<&Mesh>() + .into_iter() + .map(|(entity, (transform, info))| { + let (_, _, translation) = transform.to_scale_rotation_translation(); + EditorEntity { + name: info.name.clone(), + id: entity.to_bits().get(), + transform: Transform { + translation: translation.into(), + }, + } + }) + .collect(); + + let scene = hotham_editor_protocol::scene::Scene { + name: "Simple Scene".to_string(), + entities, + }; + + editor.send_json(&scene).unwrap(); // TODO: error types + + let editor_updates: EditorUpdates = editor.get_json().unwrap(); // TODO: error types + for entity in editor_updates.entity_updates { + debug!("Received update: {entity:?}"); + let mut entity_transform = world + .entity(Entity::from_bits(entity.id).unwrap()) + .unwrap() + .get::<&mut LocalTransform>() + .unwrap(); + entity_transform.translation = entity.transform.translation.into(); + } + + Ok(()) +} + fn tick(tick_data: TickData, engine: &mut Engine, _state: &mut State) { if tick_data.current_state == xr::SessionState::FOCUSED { hands_system(engine); @@ -94,7 +165,5 @@ fn add_helmet(models: &std::collections::HashMap, world: &mut Wor let collider = Collider::new(SharedShape::ball(0.35)); - world - .insert(helmet, (collider, RigidBody::default())) - .unwrap(); + world.insert_one(helmet, collider).unwrap(); } diff --git a/hotham-editor-protocol/Cargo.toml b/hotham-editor-protocol/Cargo.toml new file mode 100644 index 00000000..f4aac57b --- /dev/null +++ b/hotham-editor-protocol/Cargo.toml @@ -0,0 +1,13 @@ +[package] +edition = "2021" +name = "hotham-editor-protocol" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ash = "0.37.2" +mint = {version = "0.5.9", features = ["serde"]} +openxr-sys = "0.9.3" +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" diff --git a/hotham-editor-protocol/README.md b/hotham-editor-protocol/README.md new file mode 100644 index 00000000..4d20ab98 --- /dev/null +++ b/hotham-editor-protocol/README.md @@ -0,0 +1,39 @@ +# Hotham Editor Protocol +## Why? +Whenever two programs want to talk to eachother, the most complicated question is *where is the protocol defined?*. Sometimes the protocol is inferred from the way the programs serialise and deserialise their messages, but this leads to all sorts of problems. + +In essence, this is a *contract* problem. What is the contract, where is it, and how is it enforced? + +For more on this topic, we can, as always, defer to [Joe Armstrong (RIP)](https://www.youtube.com/watch?v=ed7A7r6DBsM). + +## How? +Simple. We define: + +- What the messages of the protocol are +- A means to **encode** them to bytes +- A means to **decode** them to bytes + +We can even take that a step further and define FSMs (as Joe would suggest), but that is future work. + + +## Examples +Let's say we're using Unix sockets: + +```rust +let socket = UnixStream::connect("hotham_editor.socket").unwrap(); +let client = EditorClient::new(socket); // woah, generics + +let view_configuration = client.request(&requests::GetViewConfiguration {}).unwrap(); // view_configuration is the correct type!! +``` + +This magic is made possible through the `Request` trait: + +```rust +pub trait Request { + /// What should the response to this request be? + type Response: Clone; + + /// Get a `RequestType` tag that we can use to identify requests + fn request_type(&self) -> RequestType; +} +``` diff --git a/hotham-editor-protocol/src/lib.rs b/hotham-editor-protocol/src/lib.rs new file mode 100644 index 00000000..46c86b95 --- /dev/null +++ b/hotham-editor-protocol/src/lib.rs @@ -0,0 +1,586 @@ +use std::io::{Read, Write}; + +pub use openxr_sys::ViewConfigurationView; +use serde::{de::DeserializeOwned, Serialize}; + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestType { + GetViewConfiguration, + GetViewCount, + GetSwapchainInfo, + GetSwapchainImages, + GetSwapchainSemaphores, + WaitFrame, + AcquireSwapchainImage, + EndFrame, + GetInputEvents, + LocateView, + InitEditor, + PutEntities, + JSON, // some arbitrary sized data +} + +pub trait Request { + type Response: Clone; + fn request_type(&self) -> RequestType; +} + +pub trait RequestWithVecResponse { + type ResponseItem: Clone; // shitty name +} + +pub mod scene { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct EditorUpdates { + pub entity_updates: Vec, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Scene { + pub name: String, + pub entities: Vec, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct EditorEntity { + pub name: String, + pub id: u64, + pub transform: Transform, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Transform { + pub translation: mint::Vector3, + } +} + +pub mod requests { + use crate::{ + responses::{InputEvent, SwapchainInfo, ViewConfiguration}, + Request, RequestType, RequestWithVecResponse, + }; + use ash::vk; + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetViewConfiguration {} + + impl Request for GetViewConfiguration { + type Response = ViewConfiguration; + fn request_type(&self) -> RequestType { + RequestType::GetViewConfiguration + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetViewCount {} + + impl Request for GetViewCount { + type Response = u32; + fn request_type(&self) -> RequestType { + RequestType::GetViewCount + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetSwapchainInfo {} + + impl Request for GetSwapchainInfo { + type Response = SwapchainInfo; + fn request_type(&self) -> RequestType { + RequestType::GetSwapchainInfo + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct WaitFrame; + + impl Request for WaitFrame { + type Response = u32; + fn request_type(&self) -> RequestType { + RequestType::WaitFrame + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct AcquireSwapchainImage; + + impl Request for AcquireSwapchainImage { + type Response = u32; + fn request_type(&self) -> RequestType { + RequestType::AcquireSwapchainImage + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct EndFrame; + + impl Request for EndFrame { + type Response = u32; + fn request_type(&self) -> RequestType { + RequestType::EndFrame + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct LocateView; + + impl Request for LocateView { + type Response = openxr_sys::Posef; + fn request_type(&self) -> RequestType { + RequestType::LocateView + } + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetSwapchainImages {} + + impl Request for GetSwapchainImages { + type Response = bool; // TODO: might need to split up the Response trait + fn request_type(&self) -> RequestType { + RequestType::GetSwapchainImages + } + } + + impl RequestWithVecResponse for GetSwapchainImages { + type ResponseItem = vk::HANDLE; + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetSwapchainSemaphores {} + + impl Request for GetSwapchainSemaphores { + type Response = bool; // TODO: might need to split up the Response trait + fn request_type(&self) -> RequestType { + RequestType::GetSwapchainSemaphores + } + } + + impl RequestWithVecResponse for GetSwapchainSemaphores { + type ResponseItem = vk::HANDLE; + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct GetInputEvents; + + impl Request for GetInputEvents { + type Response = bool; // TODO: might need to split up the Response trait + fn request_type(&self) -> RequestType { + RequestType::GetInputEvents + } + } + + impl RequestWithVecResponse for GetInputEvents { + type ResponseItem = InputEvent; + } + + #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct PutEntities; + + impl Request for PutEntities { + type Response = bool; // TODO: might need to split up the Response trait + fn request_type(&self) -> RequestType { + RequestType::PutEntities + } + } +} + +pub mod responses { + use ash::vk; + + #[repr(C)] + #[derive(Debug, Clone, Copy)] + pub struct ViewConfiguration { + pub width: u32, + pub height: u32, + } + + #[repr(C)] + #[derive(Debug, Copy, Clone)] + pub struct SwapchainInfo { + pub resolution: vk::Extent2D, + pub format: vk::Format, + } + + #[repr(C)] + #[derive(Debug, Clone, Copy)] + pub enum InputEvent { + AButtonPressed, + AButtonReleased, + BButtonPressed, + BButtonReleased, + XButtonPressed, + XButtonReleased, + YButtonPressed, + YButtonReleased, + } +} + +#[derive(Debug, Clone)] +pub struct RequestHeader { + pub payload_length: u32, + pub request_type: RequestType, +} + +impl From for Vec { + fn from(h: RequestHeader) -> Self { + unsafe { bytes_from_t(&h).to_vec() } + } +} + +fn write_request(request: &R, writer: &mut S) -> std::io::Result<()> { + let header = RequestHeader { + request_type: request.request_type(), + payload_length: std::mem::size_of::() as u32, + }; + + writer.write_all(&{ unsafe { bytes_from_t(&header) } })?; + writer.write_all(&{ unsafe { bytes_from_t(request) } })?; + + Ok(()) +} + +fn read_request_header(reader: &mut S, buf: &mut [u8]) -> std::io::Result { + reader.read_exact(&mut buf[..std::mem::size_of::()])?; + let header: RequestHeader = + unsafe { t_from_bytes(&buf[..std::mem::size_of::()]) }; + Ok(header) +} + +fn read_request_payload( + reader: &mut S, + buf: &mut [u8], + payload_length: usize, +) -> std::io::Result { + reader.read_exact(&mut buf[..payload_length])?; + let payload = &buf[..payload_length]; + Ok(unsafe { t_from_bytes(payload) }) +} + +pub struct EditorClient { + socket: S, + buffer: Vec, +} + +impl EditorClient { + pub fn new(socket: S) -> Self { + Self { + socket, + buffer: vec![0; 1024 * 1024], + } + } + + pub fn request(&mut self, request: &R) -> std::io::Result { + self.send_request(request)?; + self.get_response::() + } + + pub fn request_vec( + &mut self, + request: &R, + ) -> std::io::Result> { + self.send_request(request)?; + self.get_response_vec::() + } + + pub fn send_request(&mut self, request: &R) -> std::io::Result<()> { + write_request(request, &mut self.socket) + } + + pub fn get_json(&mut self) -> serde_json::Result { + let request_header = read_request_header(&mut self.socket, &mut self.buffer).unwrap(); // TODO error types + assert_eq!(request_header.request_type, RequestType::JSON); + let buffer = &mut self.buffer[..request_header.payload_length as _]; + self.socket.read_exact(buffer).unwrap(); // TODO: error types + serde_json::from_slice(buffer) + } + + pub fn send_json( + &mut self, + value: &J, + ) -> serde_json::Result<()> { + let json_bytes = serde_json::to_vec(value)?; + let header = RequestHeader { + request_type: RequestType::JSON, + payload_length: json_bytes.len() as u32, + }; + + self.socket + .write_all(&{ unsafe { bytes_from_t(&header) } }) + .unwrap(); // TODO error types + self.socket.write_all(&json_bytes).unwrap(); // TODO error types + + Ok(()) + } + + pub fn get_response(&mut self) -> std::io::Result { + let socket = &mut self.socket; + let buf = &mut self.buffer; + + let header_size = std::mem::size_of::(); + socket.read_exact(&mut buf[..header_size])?; + let message_size = u32::from_be_bytes(buf[..header_size].try_into().unwrap()) as usize; + + self.socket.read_exact(&mut buf[..message_size])?; + Ok(unsafe { t_from_bytes(&buf[..message_size]) }) + } + + pub fn get_response_vec(&mut self) -> std::io::Result> { + let socket = &mut self.socket; + let buf = &mut self.buffer; + + let header_size = std::mem::size_of::(); + socket.read_exact(&mut buf[..header_size])?; + let message_size = u32::from_be_bytes(buf[..header_size].try_into().unwrap()) as usize; + + self.socket.read_exact(&mut buf[..message_size])?; + Ok(unsafe { vec_from_bytes(&buf[..message_size]) }) + } +} + +pub struct EditorServer { + socket: S, + buffer: Vec, +} + +impl EditorServer { + pub fn new(socket: S) -> Self { + Self { + socket, + buffer: vec![0; 1024 * 1024], + } + } + + /// Helpful if you already know in advance what the request type is + pub fn get_request(&mut self) -> std::io::Result { + let request_header = read_request_header(&mut self.socket, &mut self.buffer)?; + read_request_payload( + &mut self.socket, + &mut self.buffer, + request_header.payload_length as usize, + ) + } + + pub fn get_json(&mut self) -> serde_json::Result { + let request_header = self.get_request_header().unwrap(); // TODO error types + assert_eq!(request_header.request_type, RequestType::JSON); + let buffer = &mut self.buffer[..request_header.payload_length as _]; + self.socket.read_exact(buffer).unwrap(); // TODO: error types + serde_json::from_slice(buffer) + } + + pub fn send_json( + &mut self, + value: &J, + ) -> serde_json::Result<()> { + let json_bytes = serde_json::to_vec(value)?; + let header = RequestHeader { + request_type: RequestType::JSON, + payload_length: json_bytes.len() as u32, + }; + + self.socket + .write_all(&{ unsafe { bytes_from_t(&header) } }) + .unwrap(); // TODO error types + self.socket.write_all(&json_bytes).unwrap(); // TODO error types + + Ok(()) + } + + pub fn get_request_header(&mut self) -> std::io::Result { + read_request_header(&mut self.socket, &mut self.buffer) + } + + pub fn get_request_payload( + &mut self, + payload_length: u32, + ) -> std::io::Result { + read_request_payload(&mut self.socket, &mut self.buffer, payload_length as usize) + } + + pub fn send_response(&mut self, response: &T) -> std::io::Result<()> { + let message_size = std::mem::size_of::() as u32; + self.socket.write_all(&message_size.to_be_bytes())?; + self.socket.write_all(&unsafe { bytes_from_t(response) })?; + + Ok(()) + } + + pub fn send_response_vec(&mut self, response: &Vec) -> std::io::Result<()> { + let message_size = (std::mem::size_of::() * response.len()) as u32; + self.socket.write_all(&message_size.to_be_bytes())?; + self.socket + .write_all(&unsafe { bytes_from_vec(response) })?; + + Ok(()) + } +} +unsafe fn bytes_from_vec(data: &[T]) -> Vec { + let len = std::mem::size_of::() * data.len(); + std::slice::from_raw_parts(data as *const _ as *const u8, len).to_vec() +} + +unsafe fn vec_from_bytes(data: &[u8]) -> Vec { + let len = data.len() / std::mem::size_of::(); + std::slice::from_raw_parts(data.as_ptr().cast(), len).to_vec() +} + +// Clippy knows not what evil we do +#[allow(clippy::redundant_clone)] +unsafe fn t_from_bytes(data: &[u8]) -> T { + std::ptr::read(data.as_ptr().cast::()).clone() +} + +unsafe fn bytes_from_t(data: &T) -> Vec { + std::slice::from_raw_parts(data as *const _ as *const u8, std::mem::size_of::()).to_vec() +} + +#[cfg(test)] +mod tests { + use crate::requests::GetViewConfiguration; + + use super::*; + use openxr_sys::{StructureType, ViewConfigurationView}; + use serde::Deserialize; + use std::{cell::RefCell, rc::Rc}; + + #[derive(Default, Clone)] + struct MockSocket { + pub data: Rc>>, + } + + impl MockSocket { + pub fn reset(&self) { + self.data.borrow_mut().clear(); + } + } + + impl Write for MockSocket { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.data.borrow_mut().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + impl Read for MockSocket { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut data = self.data.borrow_mut(); + let read_length = buf.len(); + buf.copy_from_slice(&data[..buf.len()]); + data.rotate_left(read_length); // shuffle the bytes that we just read back over to the end + Ok(buf.len()) + } + } + + #[test] + pub fn test_request_response() { + let socket = MockSocket::default(); + let mut client = EditorClient::new(socket.clone()); + let mut server = EditorServer::new(socket); + + let request = GetViewConfiguration {}; + client.send_request(&request).unwrap(); + let request_header = server.get_request_header().unwrap(); + match request_header.request_type { + RequestType::GetViewConfiguration => { + let request_from_client: GetViewConfiguration = server + .get_request_payload(request_header.payload_length) + .unwrap(); + assert_eq!(request, request_from_client) + } + _ => panic!("Bad request"), + }; + + server.socket.reset(); + + let response = ViewConfigurationView { + ty: StructureType::VIEW_CONFIGURATION_VIEW, + next: std::ptr::null_mut(), + recommended_image_rect_width: 100, + max_image_rect_width: 100, + recommended_image_rect_height: 100, + max_image_rect_height: 100, + recommended_swapchain_sample_count: 100, + max_swapchain_sample_count: 100, + }; + + server.send_response(&response).unwrap(); + let response_from_server: ViewConfigurationView = client.get_response().unwrap(); + assert_eq!(response.ty, response_from_server.ty); + assert_eq!( + response.max_swapchain_sample_count, + response_from_server.max_swapchain_sample_count + ); + + server.socket.reset(); + + let response = [ + ViewConfigurationView { + ty: StructureType::VIEW_CONFIGURATION_VIEW, + next: std::ptr::null_mut(), + recommended_image_rect_width: 100, + max_image_rect_width: 100, + recommended_image_rect_height: 100, + max_image_rect_height: 100, + recommended_swapchain_sample_count: 100, + max_swapchain_sample_count: 100, + }, + ViewConfigurationView { + ty: StructureType::VIEW_CONFIGURATION_VIEW, + next: std::ptr::null_mut(), + recommended_image_rect_width: 200, + max_image_rect_width: 100, + recommended_image_rect_height: 100, + max_image_rect_height: 100, + recommended_swapchain_sample_count: 100, + max_swapchain_sample_count: 100, + }, + ]; + + server.send_response(&response).unwrap(); + let response_from_server: Vec = client.get_response_vec().unwrap(); + assert_eq!(response.len(), response_from_server.len()); + assert_eq!( + response[0].max_swapchain_sample_count, + response_from_server[0].max_swapchain_sample_count + ); + assert_eq!( + response[1].max_swapchain_sample_count, + response_from_server[1].max_swapchain_sample_count + ); + } + + #[test] + pub fn test_json() -> std::io::Result<()> { + let socket = MockSocket::default(); + let mut client = EditorClient::new(socket.clone()); + let mut server = EditorServer::new(socket); + + #[derive(Serialize, Deserialize, Clone)] + struct GetThing { + thing_amount: usize, + } + + client.send_json(&GetThing { thing_amount: 5 }).unwrap(); + let request: GetThing = server.get_json().unwrap(); + assert_eq!(request.thing_amount, 5); + + Ok(()) + } +} diff --git a/hotham-editor/Cargo.toml b/hotham-editor/Cargo.toml new file mode 100644 index 00000000..5a5ed345 --- /dev/null +++ b/hotham-editor/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition = "2021" +name = "hotham-editor" +version = "0.1.0" + +[dependencies] +anyhow = "1.0.69" +ash = "0.37.2" +env_logger = "0.10.0" +glam = "0.22.0" +hotham-editor-protocol = {path = "../hotham-editor-protocol"} +lazy_vulkan = {git = "https://github.com/leetvr/lazy_vulkan", rev = "4e8b4982328f7bda76b10e6b7e83c65398917fef"}# main @ 20/2/2023 +log = "0.4.17" +openxr-sys = "0.9.3" +winit = "0.28.1" +yakui = {git = "https://github.com/leetvr/yakui", rev = "a562328"}#add-texture-from-image branch @ 1/3/23 +yakui-vulkan = {git = "https://github.com/leetvr/yakui", rev = "a562328"}#add-texture-from-image branch @ 1/3/23 +yakui-winit = {git = "https://github.com/leetvr/yakui", rev = "a562328"}#add-texture-from-image branch @ 1/3/23 + +[target.'cfg(windows)'.dependencies] +uds_windows = "1.0.2" diff --git a/hotham-editor/README.md b/hotham-editor/README.md new file mode 100644 index 00000000..73677c74 --- /dev/null +++ b/hotham-editor/README.md @@ -0,0 +1,22 @@ +# Hotham Editor +> 🚧 **UNDER CONSTRUCTION** 🚧 +> +> **WARNING**: Even more so than the rest of Hotham, this crate is under *heavy* construction. While it is technically usable in its current state its design may change: at any time. You have been warned! + +## Future plans +The ideal state for the Hotham editor is that it will make it easier to build VR games. That's a pretty ambitious goal, but that is, after all, the whole *point* of Hotham. + +Game Editors are notoriously tricky pieces of technology to define, but here are a couple of things we'd like to be able to do: + +1. Define the *initial state* for some part of a game (eg. a "scene" or a "level") +1. Inspect the *current state* of the game to debug it + +That shouldn't be too hard, right? + +It's also worth noting that the editor will *completely replace* Hotham simulator. + +## Current state +Currently, it's possible to do the following: + +- Run `simple-scene-example` with the `editor` feature +- Manipulate the transform of an entity diff --git a/hotham-editor/src/camera.rs b/hotham-editor/src/camera.rs new file mode 100644 index 00000000..089ef602 --- /dev/null +++ b/hotham-editor/src/camera.rs @@ -0,0 +1,77 @@ +use std::time::Instant; + +use glam::{Quat, Vec3}; +use winit::event::KeyboardInput; + +use crate::{input_context::InputContext, MouseInput}; + +#[derive(Debug, Clone)] +pub struct Camera { + pose: Pose, + input_context: InputContext, +} + +impl Default for Camera { + fn default() -> Self { + let pose = Pose { + position: [0., 1.4, 0.].into(), + pitch: 0., + yaw: 0., + }; + Self { + pose, + input_context: Default::default(), + } + } +} + +impl Camera { + pub fn as_pose(&self) -> openxr_sys::Posef { + (&self.pose).into() + } + + pub fn process_input( + &mut self, + last_frame_time: Instant, + keyboard_input: &[KeyboardInput], + mouse_input: &[MouseInput], + ) { + let delta_time = (Instant::now() - last_frame_time).as_secs_f32(); + self.input_context + .update(delta_time, keyboard_input, mouse_input, &mut self.pose) + } +} + +#[derive(Debug, Clone)] +pub struct Pose { + pub position: Vec3, + pub pitch: f32, + pub yaw: f32, +} + +impl Pose { + pub fn orientation(&self) -> Quat { + Quat::from_euler(glam::EulerRot::YXZ, self.yaw, self.pitch, 0.) + } +} + +impl From<&Pose> for openxr_sys::Posef { + fn from(pose: &Pose) -> Self { + let p = pose.position; + let o = pose.orientation(); + + openxr_sys::Posef { + orientation: openxr_sys::Quaternionf { + x: o.x, + y: o.y, + z: o.z, + w: o.w, + }, + position: openxr_sys::Vector3f { + x: p.x, + y: p.y, + z: p.z, + }, + } + } +} diff --git a/hotham-editor/src/gui.rs b/hotham-editor/src/gui.rs new file mode 100644 index 00000000..8e555c98 --- /dev/null +++ b/hotham-editor/src/gui.rs @@ -0,0 +1,56 @@ +use glam::Vec2; +use hotham_editor_protocol::scene::EditorEntity; +use yakui::widgets::{List, Pad}; +use yakui::{ + column, image, label, pad, row, slider, text, use_state, CrossAxisAlignment, TextureId, +}; + +pub fn gui(gui_state: &mut GuiState) { + let scene = &gui_state.scene; + let updates = &mut gui_state.updates; + row(|| { + column(|| { + image(gui_state.texture_id, Vec2::new(500., 500.)); + }); + pad(Pad::all(20.0), || { + let mut column = List::column(); + column.cross_axis_alignment = CrossAxisAlignment::Start; + + column.show(|| { + text(42., scene.name.clone()); + for entity in &scene.entities { + row(|| { + label("Name"); + }); + row(|| { + text(20., entity.name.clone()); + }); + row(|| { + label("Translation"); + }); + row(|| { + yakui::column(|| { + label("x"); + let x = entity.transform.translation.x as f64; + let x_state = use_state(move || x); + label(x_state.get().to_string()); + + if let Some(new_x) = slider(x_state.get(), -5.0, 5.0).value { + let mut new_entity = entity.clone(); + x_state.set(new_x); + new_entity.transform.translation.x = new_x as _; + updates.push(new_entity); + } + }); + }); + } + }); + }); + }); +} + +pub struct GuiState { + pub texture_id: TextureId, + pub scene: hotham_editor_protocol::scene::Scene, + pub updates: Vec, +} diff --git a/hotham-editor/src/input_context.rs b/hotham-editor/src/input_context.rs new file mode 100644 index 00000000..d13399b9 --- /dev/null +++ b/hotham-editor/src/input_context.rs @@ -0,0 +1,183 @@ +use glam::{Vec2, Vec3}; +use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; + +use crate::{camera::Pose, MouseInput}; + +const INPUT_SPEED: f32 = 10.; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputContext { + keyboard_state: KeyboardState, + mouse_state: MouseState, +} + +impl Default for InputContext { + fn default() -> Self { + Self { + keyboard_state: KeyboardState::Idle, + mouse_state: MouseState::HoldingLeftClick, + } + } +} + +impl InputContext { + pub fn update( + &mut self, + delta_time: f32, + keyboard_input: &[KeyboardInput], + mouse_input: &[MouseInput], + pose: &mut Pose, + ) { + let movement_speed = INPUT_SPEED * delta_time; + + let mut mouse_motion = Vec2::ZERO; + for event in mouse_input { + match event { + MouseInput::LeftClickPressed => self.mouse_state = MouseState::HoldingLeftClick, + MouseInput::LeftClickReleased => self.mouse_state = MouseState::Idle, + MouseInput::MouseMoved(delta) => { + if self.mouse_state == MouseState::HoldingLeftClick { + mouse_motion += *delta; + } + } + }; + } + handle_mouse_movement(mouse_motion * movement_speed, pose); + + let mut keyboard_motion = Vec3::ZERO; + for event in keyboard_input { + //safe as we only receive events with a keycode + let (state, key) = (event.state, event.virtual_keycode.unwrap()); + + match state { + ElementState::Pressed => { + self.keyboard_state = KeyboardState::HoldingKey(key); + keyboard_motion += handle_keypress(key, pose); + } + ElementState::Released => { + self.keyboard_state = KeyboardState::Idle; + } + } + } + + // If there were no keyboard inputs, but we're still holding down a key, act as if that key was pressed + if let (KeyboardState::HoldingKey(key), true) = + (&self.keyboard_state, keyboard_input.is_empty()) + { + keyboard_motion = handle_keypress(*key, pose); + } + + pose.position += keyboard_motion * movement_speed; + } +} + +fn handle_mouse_movement(movement: Vec2, pose: &mut Pose) { + pose.yaw -= movement.x; + const MIN_PITCH: f32 = -std::f32::consts::FRAC_PI_4; + const MAX_PITCH: f32 = std::f32::consts::FRAC_PI_4; + pose.pitch = (pose.pitch - movement.y).clamp(MIN_PITCH, MAX_PITCH); +} + +fn handle_keypress(key: VirtualKeyCode, pose: &mut Pose) -> Vec3 { + let orientation = pose.orientation(); + let mut position = Vec3::ZERO; + // get the forward vector rotated by the camera rotation quaternion + let forward = orientation * Vec3::NEG_Z; + // get the right vector rotated by the camera rotation quaternion + let right = orientation * Vec3::X; + let up = Vec3::Y; + + match key { + winit::event::VirtualKeyCode::W => { + position.x += forward.x; + position.y += forward.y; + position.z += forward.z; + } + winit::event::VirtualKeyCode::S => { + position.x -= forward.x; + position.y -= forward.y; + position.z -= forward.z; + } + winit::event::VirtualKeyCode::A => { + position.x -= right.x; + position.y -= right.y; + position.z -= right.z; + } + winit::event::VirtualKeyCode::D => { + position.x += right.x; + position.y += right.y; + position.z += right.z; + } + winit::event::VirtualKeyCode::Space => { + position.y += up.y; + } + winit::event::VirtualKeyCode::LShift => { + position.y -= up.y; + } + _ => {} + } + + position +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum KeyboardState { + Idle, + HoldingKey(VirtualKeyCode), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum MouseState { + Idle, + HoldingLeftClick, +} + +#[cfg(test)] +#[allow(deprecated)] +mod tests { + use winit::event::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode}; + + use crate::{camera::Pose, input_context::INPUT_SPEED}; + + use super::InputContext; + + #[test] + pub fn test_keyboard_input() { + let mut input_context = InputContext::default(); + let mut pose = Pose { + position: Default::default(), + pitch: 0., + yaw: 0., + }; + + // press w + input_context.update(1.0, &[press(VirtualKeyCode::W)], &[], &mut pose); + assert_eq!(pose.position, [0., 0., -INPUT_SPEED].into()); + + // keep holding it + input_context.update(1.0, &[], &[], &mut pose); + assert_eq!(pose.position, [0., 0., -INPUT_SPEED * 2.0].into()); + + // release + input_context.update(1.0, &[release(VirtualKeyCode::W)], &[], &mut pose); + assert_eq!(pose.position, [0., 0., -INPUT_SPEED * 2.0].into()); + } + + fn press(virtual_code: VirtualKeyCode) -> KeyboardInput { + KeyboardInput { + scancode: 0, + state: ElementState::Pressed, + virtual_keycode: Some(virtual_code), + modifiers: ModifiersState::empty(), + } + } + + fn release(virtual_code: VirtualKeyCode) -> KeyboardInput { + KeyboardInput { + scancode: 0, + state: ElementState::Released, + virtual_keycode: Some(virtual_code), + modifiers: ModifiersState::empty(), + } + } +} diff --git a/hotham-editor/src/main.rs b/hotham-editor/src/main.rs new file mode 100644 index 00000000..d154a20e --- /dev/null +++ b/hotham-editor/src/main.rs @@ -0,0 +1,537 @@ +mod camera; +mod gui; +mod input_context; + +use anyhow::{bail, Result}; +use ash::vk; + +use glam::Vec2; +use hotham_editor_protocol::{responses, scene::EditorUpdates, EditorServer, RequestType}; +use lazy_vulkan::{ + find_memorytype_index, vulkan_context::VulkanContext, vulkan_texture::VulkanTexture, + LazyRenderer, LazyVulkan, SwapchainInfo, Vertex, +}; +use log::{debug, info, trace}; +use yakui_winit::YakuiWinit; + +use std::time::Instant; +use uds_windows::{UnixListener, UnixStream}; +use winit::{ + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::ControlFlow, + platform::run_return::EventLoopExtRunReturn, +}; + +use crate::{ + camera::Camera, + gui::{gui, GuiState}, +}; + +static FRAGMENT_SHADER: &'_ [u8] = include_bytes!("shaders/triangle.frag.spv"); +static VERTEX_SHADER: &'_ [u8] = include_bytes!("shaders/triangle.vert.spv"); +const SWAPCHAIN_FORMAT: vk::Format = vk::Format::R8G8B8A8_SRGB; // OpenXR really, really wants us to use sRGB swapchains +static UNIX_SOCKET_PATH: &'_ str = "hotham_editor.socket"; + +pub fn main() -> Result<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + let vertices = [ + Vertex::new([1.0, 1.0, 0.0, 1.0], [1.0, 1.0, 1.0, 0.0], [1.0, 1.0]), // bottom right + Vertex::new([-1.0, 1.0, 0.0, 1.0], [1.0, 1.0, 1.0, 0.0], [0.0, 1.0]), // bottom left + Vertex::new([1.0, -1.0, 0.0, 1.0], [1.0, 1.0, 1.0, 0.0], [1.0, 0.0]), // top right + Vertex::new([-1.0, -1.0, 0.0, 1.0], [1.0, 1.0, 1.0, 0.0], [0.0, 0.0]), // top left + ]; + + // Your own index type?! What are you going to use, `u16`? + let indices = [0, 1, 2, 2, 1, 3]; + + let window_size = vk::Extent2D { + width: 800, + height: 500, + }; + + // Let's do something totally normal and wait for a TCP connection + if std::fs::remove_file(UNIX_SOCKET_PATH).is_ok() { + debug!("Removed pre-existing unix socket at {UNIX_SOCKET_PATH}"); + } + + let listener = UnixListener::bind(UNIX_SOCKET_PATH).unwrap(); + info!("Listening on {UNIX_SOCKET_PATH}: waiting for game client.."); + let (stream, _) = listener.accept().unwrap(); + let mut game_server = EditorServer::new(stream); + + info!("Game connected! Waiting for OpenXR client..",); + let (stream, _) = listener.accept().unwrap(); + let mut openxr_server = EditorServer::new(stream); + info!("OpenXR client connected! Opening window and doing OpenXR setup"); + + // Alright, let's build some stuff + let (mut lazy_vulkan, mut lazy_renderer, mut event_loop) = LazyVulkan::builder() + .initial_vertices(&vertices) + .initial_indices(&indices) + .fragment_shader(FRAGMENT_SHADER) + .vertex_shader(VERTEX_SHADER) + .with_present(true) + .window_size(window_size) + .build(); + let swapchain_info = SwapchainInfo { + image_count: lazy_vulkan.surface.desired_image_count, + resolution: vk::Extent2D { + width: 500, + height: 500, + }, + format: SWAPCHAIN_FORMAT, + }; + let xr_swapchain = do_openxr_setup(&mut openxr_server, lazy_vulkan.context(), &swapchain_info)?; + info!("..done!"); + let mut yak_images = create_render_textures( + lazy_vulkan.context(), + &mut lazy_renderer, + xr_swapchain.images, + ); + + let window = &lazy_vulkan.window; + + let mut last_frame_time = Instant::now(); + let mut keyboard_events = Vec::new(); + let mut mouse_events = Vec::new(); + let mut camera = Camera::default(); + let mut yak = yakui::Yakui::new(); + let mut yakui_window = YakuiWinit::new(window); + + let (mut yakui_vulkan, yak_images) = { + let context = lazy_vulkan.context(); + let yakui_vulkan_context = yakui_vulkan::VulkanContext::new( + &context.device, + context.queue, + context.draw_command_buffer, + context.command_pool, + context.memory_properties, + ); + let render_surface = yakui_vulkan::RenderSurface { + resolution: window_size, + format: lazy_vulkan.surface.surface_format.format, + image_views: lazy_renderer.render_surface.image_views.clone(), + }; + let mut yakui_vulkan = + yakui_vulkan::YakuiVulkan::new(&yakui_vulkan_context, render_surface); + let yak_images = yak_images + .drain(..) + .map(|t| { + let yak_texture = lazy_to_yak(&yakui_vulkan_context, yakui_vulkan.descriptors(), t); + yakui_vulkan.add_user_texture(yak_texture) + }) + .collect::>(); + + (yakui_vulkan, yak_images) + }; + + // Off we go! + let mut winit_initializing = true; + let mut focused = false; + let mut right_mouse_clicked = false; + let mut updates = EditorUpdates { + entity_updates: vec![], + }; + + event_loop.run_return(|event, _, control_flow| { + *control_flow = ControlFlow::Poll; + yakui_window.handle_event(&mut yak, &event); + match event { + Event::WindowEvent { + event: + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + }, + .. + }, + .. + } => *control_flow = ControlFlow::Exit, + + Event::WindowEvent { + event: WindowEvent::Focused(window_focused), + .. + } => { + focused = window_focused; + } + + Event::NewEvents(cause) => { + if cause == winit::event::StartCause::Init { + winit_initializing = true; + } else { + winit_initializing = false; + } + } + + Event::MainEventsCleared => { + let framebuffer_index = lazy_vulkan.render_begin(); + camera.process_input(last_frame_time, &keyboard_events, &mouse_events); + keyboard_events.clear(); + mouse_events.clear(); + + check_request(&mut openxr_server, RequestType::LocateView).unwrap(); + openxr_server.send_response(&camera.as_pose()).unwrap(); + + check_request(&mut openxr_server, RequestType::WaitFrame).unwrap(); + openxr_server.send_response(&0).unwrap(); + + check_request(&mut openxr_server, RequestType::AcquireSwapchainImage).unwrap(); + openxr_server.send_response(&framebuffer_index).unwrap(); + + let scene: hotham_editor_protocol::scene::Scene = game_server.get_json().unwrap(); + let mut gui_state = GuiState { texture_id: yak_images[framebuffer_index as usize], scene, updates: vec![] }; + game_server + .send_json(&updates) + .unwrap(); + + updates.entity_updates.clear(); + + // game has finished rendering its frame here + + check_request(&mut openxr_server, RequestType::LocateView).unwrap(); + openxr_server.send_response(&camera.as_pose()).unwrap(); + + check_request(&mut openxr_server, RequestType::EndFrame).unwrap(); + openxr_server.send_response(&0).unwrap(); + + + yak.start(); + gui(&mut gui_state); + yak.finish(); + + updates.entity_updates = gui_state.updates; + + + let context = lazy_vulkan.context(); + let yakui_vulkan_context = yakui_vulkan::VulkanContext::new( + &context.device, + context.queue, + context.draw_command_buffer, + context.command_pool, + context.memory_properties, + ); + + yakui_vulkan.paint(&mut yak, &yakui_vulkan_context, framebuffer_index); + + let semaphore = xr_swapchain.semaphores[framebuffer_index as usize]; + lazy_vulkan.render_end( + framebuffer_index, + &[semaphore, lazy_vulkan.rendering_complete_semaphore], + ); + last_frame_time = Instant::now(); + right_mouse_clicked = false; + } + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + if !winit_initializing { + let new_render_surface = lazy_vulkan.resized(size.width, size.height); + let render_surface = yakui_vulkan::RenderSurface { + resolution: new_render_surface.resolution, + format: new_render_surface.format, + image_views: new_render_surface.image_views, + }; + yakui_vulkan.update_surface(render_surface, &lazy_vulkan.context().device); + yak.set_surface_size([size.width as f32, size.height as f32].into()); + } + } + Event::WindowEvent { + event: + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + .. + } => { + debug!("Scale factor changed! Scale factor: {scale_factor}, new inner size: {new_inner_size:?}"); + } + Event::WindowEvent { + event: WindowEvent::KeyboardInput { input, .. }, + .. + } => { + if focused && input.virtual_keycode.is_some() { + keyboard_events.push(input); + } + } + Event::WindowEvent { + event: WindowEvent::CursorMoved { .. }, + .. + } => { + focused = true; + } + Event::WindowEvent { + event: WindowEvent::CursorLeft { .. }, + .. + } => { + focused = false; + } + Event::DeviceEvent { event, .. } => match event { + winit::event::DeviceEvent::MouseMotion { delta } => { + if focused { + mouse_events.push(MouseInput::MouseMoved( + [delta.0 as f32, delta.1 as f32].into(), + )) + } + } + winit::event::DeviceEvent::Button { + button: 3, + state: ElementState::Pressed, + } => { + debug!("Right mouse clicked"); + right_mouse_clicked = true; + } + + winit::event::DeviceEvent::Button { + button: 1, + state: ElementState::Pressed, + } => { + if focused { + mouse_events.push(MouseInput::LeftClickPressed) + } + } + winit::event::DeviceEvent::Button { + button: 1, + state: ElementState::Released, + } => { + if focused { + mouse_events.push(MouseInput::LeftClickReleased) + } + } + _ => {} + }, + _ => (), + } + }); + + // I guess we better do this or else the Dreaded Validation Layers will complain + unsafe { + lazy_renderer.cleanup(&lazy_vulkan.context().device); + } + + Ok(()) +} + +fn lazy_to_yak( + yakui_vulkan_context: &yakui_vulkan::VulkanContext, + descriptors: &mut yakui_vulkan::Descriptors, + t: VulkanTexture, +) -> yakui_vulkan::VulkanTexture { + yakui_vulkan::VulkanTexture::from_image( + yakui_vulkan_context, + descriptors, + t.image, + t.memory, + t.view, + ) +} + +pub enum MouseInput { + LeftClickPressed, + LeftClickReleased, + MouseMoved(Vec2), +} + +pub struct XrSwapchain { + images: Vec, + semaphores: Vec, +} + +fn do_openxr_setup( + server: &mut EditorServer, + vulkan_context: &VulkanContext, + swapchain_info: &SwapchainInfo, +) -> Result { + let (images, image_memory_handles) = + unsafe { create_render_images(vulkan_context, swapchain_info) }; + let (semaphores, semaphore_handles) = + unsafe { create_semaphores(vulkan_context, swapchain_info.image_count) }; + + check_request(server, RequestType::GetViewCount)?; + server.send_response(&swapchain_info.image_count)?; + + check_request(server, RequestType::GetViewConfiguration)?; + server.send_response(&responses::ViewConfiguration { + width: swapchain_info.resolution.width, + height: swapchain_info.resolution.height, + })?; + + check_request(server, RequestType::GetSwapchainInfo)?; + server.send_response(&responses::SwapchainInfo { + format: swapchain_info.format, + resolution: swapchain_info.resolution, + })?; + + check_request(server, RequestType::GetSwapchainImages)?; + server.send_response_vec(&image_memory_handles)?; + + check_request(server, RequestType::GetSwapchainSemaphores)?; + server.send_response_vec(&semaphore_handles)?; + + Ok(XrSwapchain { images, semaphores }) +} + +fn check_request( + server: &mut EditorServer, + expected_request_type: RequestType, +) -> Result<(), anyhow::Error> { + let header = server.get_request_header()?; + if header.request_type != expected_request_type { + bail!("Invalid request type: {:?}!", header.request_type); + } + trace!("Received request from cilent {expected_request_type:?}"); + Ok(()) +} + +unsafe fn create_semaphores( + context: &lazy_vulkan::vulkan_context::VulkanContext, + image_count: u32, +) -> (Vec, Vec) { + let device = &context.device; + let external_semaphore = + ash::extensions::khr::ExternalSemaphoreWin32::new(&context.instance, &context.device); + let handle_type = vk::ExternalSemaphoreHandleTypeFlags::OPAQUE_WIN32_KMT; + (0..image_count) + .map(|_| { + let mut external_semaphore_info = + vk::ExportSemaphoreCreateInfo::builder().handle_types(handle_type); + let semaphore = device + .create_semaphore( + &vk::SemaphoreCreateInfo::builder().push_next(&mut external_semaphore_info), + None, + ) + .unwrap(); + + let handle = external_semaphore + .get_semaphore_win32_handle( + &vk::SemaphoreGetWin32HandleInfoKHR::builder() + .handle_type(handle_type) + .semaphore(semaphore), + ) + .unwrap(); + + (semaphore, handle) + }) + .unzip() +} + +fn create_render_textures( + vulkan_context: &lazy_vulkan::vulkan_context::VulkanContext, + renderer: &mut LazyRenderer, + mut images: Vec, +) -> Vec { + let descriptors = &mut renderer.descriptors; + let address_mode = vk::SamplerAddressMode::REPEAT; + let filter = vk::Filter::LINEAR; + images + .drain(..) + .map(|image| { + let view = unsafe { vulkan_context.create_image_view(image, SWAPCHAIN_FORMAT) }; + let sampler = unsafe { + vulkan_context + .device + .create_sampler( + &vk::SamplerCreateInfo::builder() + .address_mode_u(address_mode) + .address_mode_v(address_mode) + .address_mode_w(address_mode) + .mag_filter(filter) + .min_filter(filter), + None, + ) + .unwrap() + }; + + let id = + unsafe { descriptors.update_texture_descriptor_set(view, sampler, vulkan_context) }; + + lazy_vulkan::vulkan_texture::VulkanTexture { + image, + memory: vk::DeviceMemory::null(), // todo + sampler, + view, + id, + } + }) + .collect() +} + +unsafe fn create_render_images( + context: &lazy_vulkan::vulkan_context::VulkanContext, + swapchain_info: &SwapchainInfo, +) -> (Vec, Vec) { + let device = &context.device; + let SwapchainInfo { + resolution, + format, + image_count, + } = swapchain_info; + let handle_type = vk::ExternalMemoryHandleTypeFlags::OPAQUE_WIN32_KMT; + + (0..(*image_count)) + .map(|_| { + let mut handle_info = + vk::ExternalMemoryImageCreateInfo::builder().handle_types(handle_type); + let image = device + .create_image( + &vk::ImageCreateInfo { + image_type: vk::ImageType::TYPE_2D, + format: *format, + extent: (*resolution).into(), + mip_levels: 1, + array_layers: 2, + samples: vk::SampleCountFlags::TYPE_1, + tiling: vk::ImageTiling::OPTIMAL, + usage: vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + sharing_mode: vk::SharingMode::EXCLUSIVE, + p_next: &mut handle_info as *mut _ as *mut _, + ..Default::default() + }, + None, + ) + .unwrap(); + + let memory_requirements = device.get_image_memory_requirements(image); + let memory_index = find_memorytype_index( + &memory_requirements, + &context.memory_properties, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ) + .expect("Unable to find suitable memory type for image"); + let mut export_handle_info = + vk::ExportMemoryAllocateInfo::builder().handle_types(handle_type); + let memory = context + .device + .allocate_memory( + &vk::MemoryAllocateInfo::builder() + .allocation_size(memory_requirements.size) + .memory_type_index(memory_index) + .push_next(&mut export_handle_info), + None, + ) + .unwrap(); + + device.bind_image_memory(image, memory, 0).unwrap(); + + let external_memory = + ash::extensions::khr::ExternalMemoryWin32::new(&context.instance, &context.device); + let handle = external_memory + .get_memory_win32_handle( + &vk::MemoryGetWin32HandleInfoKHR::builder() + .handle_type(handle_type) + .memory(memory), + ) + .unwrap(); + debug!("Created handle {handle:?}"); + + (image, handle) + }) + .unzip() +} diff --git a/hotham-editor/src/shaders/triangle.frag b/hotham-editor/src/shaders/triangle.frag new file mode 100644 index 00000000..d0eaa73f --- /dev/null +++ b/hotham-editor/src/shaders/triangle.frag @@ -0,0 +1,29 @@ +#version 450 +#define NO_TEXTURE 4294967295 +#define WORKFLOW_MAIN 0 +#define WORKFLOW_TEXT 1 + +layout (location = 0) in vec4 in_color; +layout (location = 1) in vec2 in_uv; +layout (location = 0) out vec4 out_color; + +layout(set = 0, binding = 0) uniform sampler2D textures[16]; +layout(push_constant) uniform push_constants { + uint texture_id; + uint workflow; +}; + +void main() { + if (texture_id == NO_TEXTURE) { + out_color = in_color; + return; + } + + if (workflow == WORKFLOW_TEXT) { + float coverage = texture(textures[texture_id], in_uv).r; + out_color = in_color * coverage; + } else { + vec4 user_texture = texture(textures[texture_id], in_uv); + out_color = in_color * user_texture; + } +} \ No newline at end of file diff --git a/hotham-editor/src/shaders/triangle.frag.spv b/hotham-editor/src/shaders/triangle.frag.spv new file mode 100644 index 00000000..d0241f6d Binary files /dev/null and b/hotham-editor/src/shaders/triangle.frag.spv differ diff --git a/hotham-editor/src/shaders/triangle.vert b/hotham-editor/src/shaders/triangle.vert new file mode 100644 index 00000000..77381b16 --- /dev/null +++ b/hotham-editor/src/shaders/triangle.vert @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in vec4 in_position; +layout(location = 1) in vec4 in_color; +layout(location = 2) in vec2 in_uv; + +layout(location = 0) out vec4 out_color; +layout(location = 1) out vec2 out_uv; + +void main() { + gl_Position = in_position; + out_color = in_color; + out_uv = in_uv; +} \ No newline at end of file diff --git a/hotham-editor/src/shaders/triangle.vert.spv b/hotham-editor/src/shaders/triangle.vert.spv new file mode 100644 index 00000000..8f067adc Binary files /dev/null and b/hotham-editor/src/shaders/triangle.vert.spv differ diff --git a/hotham-openxr-client/Cargo.toml b/hotham-openxr-client/Cargo.toml new file mode 100644 index 00000000..44a028a8 --- /dev/null +++ b/hotham-openxr-client/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2021" +name = "hotham_openxr_client" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ash = "0.37.2" +env_logger = "0.10.0" +hotham-editor-protocol = {path = "../hotham-editor-protocol"} +lazy_vulkan = {git = "https://github.com/leetvr/lazy_vulkan", rev = "aad2f44"}# main @ 8/2, year of our lord twenty twenty three +log = "0.4.17" +once_cell = "1.17.0" +openxr-sys = "0.9.3" +rand = "0.8.5" + +[target.'cfg(windows)'.dependencies] +uds_windows = "1.0.2" diff --git a/hotham-openxr-client/README.md b/hotham-openxr-client/README.md new file mode 100644 index 00000000..85171660 --- /dev/null +++ b/hotham-openxr-client/README.md @@ -0,0 +1,19 @@ +# Hotham OpenXR Client +This package will eventually replace the `hotham-simulator` package, along with `hotham-editor`. + +Currently, `hotham-simulator` does a lot. It: + +- Implements (most) of the OpenXR runtime +- It acts as an OpenXR server +- It controls the window and inputs for the OpenXR runtime + +This is a bit too much and is mostly a result of Kane's misunderstanding of how OpenXR works. + +# The new model +In The New Model, which is shiny and perfect and has no flaws, the OpenXR client is much more limited in scope. + +- It creates a dynamic library that the OpenXR loader can call into +- It handles Vulkan instance and device creation on behalf of the OpenXR app +- It communicates with the OpenXR server (that is, `hotham-editor`) to submit frames and check for input events + +You can see a flowchart of how this all works [here](https://www.figma.com/file/5kDF7s5wNewPQY7hw1WXsd/Hotham-Editor?node-id=0%3A1&t=iZ09gupiiA5nqFYR-1) diff --git a/hotham-openxr-client/hotham-openxr-client.json b/hotham-openxr-client/hotham-openxr-client.json new file mode 100644 index 00000000..f6a32f73 --- /dev/null +++ b/hotham-openxr-client/hotham-openxr-client.json @@ -0,0 +1,8 @@ +{ + "file_format_version": "1.0.0", + "runtime": { + "api_version": "1.0", + "name": "Hotham OpenXR Client", + "library_path": "..\\target\\debug\\hotham_openxr_client.dll" + } +} diff --git a/hotham-openxr-client/openxr_client.reg b/hotham-openxr-client/openxr_client.reg new file mode 100644 index 00000000..648f339c Binary files /dev/null and b/hotham-openxr-client/openxr_client.reg differ diff --git a/hotham-openxr-client/src/action_state.rs b/hotham-openxr-client/src/action_state.rs new file mode 100644 index 00000000..573043ff --- /dev/null +++ b/hotham-openxr-client/src/action_state.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use openxr_sys::{Action, Path, FALSE}; + +#[derive(Debug, Clone, Default)] +/// Stores Action state, allowing the simulator to simulate input for an application. +// A bit yuck to use u64 instead of Action, but it doesn't support Hash.. but whatever. +pub struct ActionState { + boolean_actions: HashMap, + _bindings: HashMap, +} +impl ActionState { + pub(crate) fn get_boolean(&self, action: Action) -> openxr_sys::Bool32 { + self.boolean_actions + .get(&action.into_raw()) + .map(|p| (*p).into()) + .unwrap_or(FALSE) + } + + pub(crate) fn _add_binding(&mut self, path: Path, action: Action) { + self._bindings.insert(path, action.into_raw()); + } + + /// Resets all action state. + pub(crate) fn _clear(&mut self) { + // Set all the booleans to false. + self.boolean_actions.values_mut().for_each(|v| *v = false); + } + + pub(crate) fn _set_boolean(&mut self, path: &Path, value: bool) { + let action = self._bindings.get(path).unwrap(); + self.boolean_actions.insert(*action, value); + } +} diff --git a/hotham-openxr-client/src/client.rs b/hotham-openxr-client/src/client.rs new file mode 100644 index 00000000..d4adb9e5 --- /dev/null +++ b/hotham-openxr-client/src/client.rs @@ -0,0 +1,1062 @@ +#![allow( + non_snake_case, + dead_code, + non_upper_case_globals, + non_camel_case_types +)] +use crate::{action_state::ActionState, space_state::SpaceState}; +use ash::vk::{self, Handle}; +use hotham_editor_protocol::{ + requests::{self, AcquireSwapchainImage, EndFrame, LocateView}, + responses::SwapchainInfo, + EditorClient, +}; +use log::{debug, error, trace}; +use once_cell::sync::OnceCell; +use openxr_sys::{ + platform::{VkDevice, VkInstance, VkPhysicalDevice, VkResult}, + Action, ActionCreateInfo, ActionSet, ActionSetCreateInfo, ActionSpaceCreateInfo, + ActionStateBoolean, ActionStateFloat, ActionStateGetInfo, ActionStatePose, ActionsSyncInfo, + EnvironmentBlendMode, EventDataBuffer, EventDataSessionStateChanged, ExtensionProperties, Fovf, + FrameBeginInfo, FrameEndInfo, FrameState, FrameWaitInfo, GraphicsBindingVulkanKHR, + GraphicsRequirementsVulkanKHR, HapticActionInfo, HapticBaseHeader, Instance, + InstanceCreateInfo, InstanceProperties, InteractionProfileSuggestedBinding, Path, Posef, + Quaternionf, ReferenceSpaceCreateInfo, ReferenceSpaceType, Result, Session, + SessionActionSetsAttachInfo, SessionBeginInfo, SessionCreateInfo, SessionState, Space, + SpaceLocation, SpaceLocationFlags, StructureType, Swapchain, SwapchainCreateInfo, + SwapchainImageAcquireInfo, SwapchainImageBaseHeader, SwapchainImageReleaseInfo, + SwapchainImageVulkanKHR, SwapchainImageWaitInfo, SystemGetInfo, SystemId, SystemProperties, + Time, Vector3f, Version, View, ViewConfigurationType, ViewConfigurationView, ViewLocateInfo, + ViewState, ViewStateFlags, VulkanDeviceCreateInfoKHR, VulkanGraphicsDeviceGetInfoKHR, + VulkanInstanceCreateInfoKHR, FALSE, TRUE, +}; + +use lazy_vulkan::vulkan_context::VulkanContext; +use std::{ + collections::HashMap, + ffi::{c_char, CStr}, + mem::transmute, + ptr::null_mut, +}; +use std::{ptr, slice, time::Instant}; +use uds_windows::UnixStream; + +type PartialVulkan = (ash::Entry, ash::Instance); +type SpaceMap = HashMap; +type StringToPathMap = HashMap; +type PathToStringMap = HashMap; +type BindingMap = HashMap; + +// Used during the init phase +static mut PARTIAL_VULKAN: OnceCell = OnceCell::new(); +static INSTANCE: OnceCell = OnceCell::new(); +static SESSION: OnceCell = OnceCell::new(); +static VULKAN_CONTEXT: OnceCell = OnceCell::new(); +static mut SPACES: OnceCell = OnceCell::new(); +static mut EDITOR_CLIENT: OnceCell> = OnceCell::new(); +static mut STRING_TO_PATH: OnceCell = OnceCell::new(); +static mut PATH_TO_STRING: OnceCell = OnceCell::new(); +static mut BINDINGS: OnceCell = OnceCell::new(); +static mut SWAPCHAIN_IMAGE_COUNT: u32 = 0; // handy to keep around +static mut SWAPCHAIN_IMAGES: OnceCell> = OnceCell::new(); +static mut SWAPCHAIN_SEMAPHORES: OnceCell> = OnceCell::new(); +static mut SESSION_STATE: SessionState = SessionState::UNKNOWN; +static mut ACTION_STATE: OnceCell = OnceCell::new(); +static CLOCK: OnceCell = OnceCell::new(); + +// Camera, etc +pub const CAMERA_FIELD_OF_VIEW: f32 = 1.; // about 57 degrees + +pub unsafe extern "system" fn enumerate_instance_extension_properties( + _layer_names: *const ::std::os::raw::c_char, + property_capacity_input: u32, + property_count_output: *mut u32, + properties: *mut ExtensionProperties, +) -> Result { + trace!("enumerate_instance_extension_properties"); + + set_array( + property_capacity_input, + property_count_output, + properties, + [ExtensionProperties { + ty: StructureType::EXTENSION_PROPERTIES, + next: ptr::null_mut(), + extension_name: str_to_fixed_bytes("XR_KHR_vulkan_enable2"), + extension_version: 1, + }], + ); + Result::SUCCESS +} + +pub unsafe extern "system" fn create_instance( + _create_info: *const InstanceCreateInfo, + instance: *mut Instance, +) -> Result { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + trace!("create_instance"); + + // Initialise our various maps + let _ = SPACES.set(Default::default()); + let _ = STRING_TO_PATH.set(Default::default()); + let _ = PATH_TO_STRING.set(Default::default()); + let _ = BINDINGS.set(Default::default()); + let _ = ACTION_STATE.set(Default::default()); + + // New instance, new luck. + *instance = Instance::from_raw(rand::random()); + let _ = INSTANCE.set(*instance); + + // Mr. Gaeta, start the clock. + CLOCK.set(Instant::now()).unwrap(); + + // Connect to the server + match UnixStream::connect("hotham_editor.socket") { + Ok(stream) => { + trace!("Successfully connected to editor!"); + drop(EDITOR_CLIENT.set(EditorClient::new(stream))); + Result::SUCCESS + } + Err(e) => { + error!("Unable to establish connection to editor: {e:?}"); + Result::ERROR_INITIALIZATION_FAILED + } + } +} + +pub unsafe extern "system" fn get_system( + _instance: Instance, + _get_info: *const SystemGetInfo, + system_id: *mut SystemId, +) -> Result { + trace!("get_system"); + // we are teh leetzor systemz + *system_id = SystemId::from_raw(1337); + Result::SUCCESS +} + +pub unsafe extern "system" fn create_vulkan_instance( + _instance: Instance, + create_info: *const VulkanInstanceCreateInfoKHR, + vulkan_instance: *mut VkInstance, + vulkan_result: *mut VkResult, +) -> Result { + trace!("create_vulkan_instance"); + + // I mean, look, we're *meant* to use the pfnGetInstanceProcAddr from the client + // but what are the odds that it's going to be any different from ours? + // + // We do care about the extensions though - they're important. + let create_info = *create_info; + + let instance_create_info: &vk::InstanceCreateInfo = &(*create_info.vulkan_create_info.cast()); + + let extension_names = if instance_create_info.enabled_extension_count > 0 { + std::slice::from_raw_parts( + instance_create_info.pp_enabled_extension_names, + instance_create_info.enabled_extension_count as _, + ) + } else { + &[] + }; + + trace!("Application requested extension names: {extension_names:?}"); + + let (entry, instance) = lazy_vulkan::vulkan_context::init(&mut extension_names.to_vec()); + *vulkan_instance = instance.handle().as_raw() as *const _; + + let _ = PARTIAL_VULKAN.set((entry, instance)); + + *vulkan_result = vk::Result::SUCCESS.as_raw(); + Result::SUCCESS +} + +pub unsafe extern "system" fn get_vulkan_graphics_device_2( + _instance: Instance, + _get_info: *const VulkanGraphicsDeviceGetInfoKHR, + vulkan_physical_device: *mut VkPhysicalDevice, +) -> Result { + trace!("get_vulkan_graphics_device_2"); + let (_, instance) = PARTIAL_VULKAN.get().unwrap(); + let physical_device = lazy_vulkan::vulkan_context::get_physical_device(instance, None, None).0; + trace!("Physical device: {physical_device:?}"); + *vulkan_physical_device = physical_device.as_raw() as *const _; + Result::SUCCESS +} + +pub unsafe extern "system" fn create_vulkan_device( + _instance: Instance, + create_info: *const VulkanDeviceCreateInfoKHR, + vulkan_device: *mut VkDevice, + vulkan_result: *mut VkResult, +) -> Result { + trace!("create_vulkan_device"); + let (_, instance) = PARTIAL_VULKAN.get().unwrap(); + let create_info = &*create_info; + let physical_device: vk::PhysicalDevice = + vk::PhysicalDevice::from_raw(create_info.vulkan_physical_device as u64); + let device_create_info: &mut vk::DeviceCreateInfo = + &mut *create_info.vulkan_create_info.cast_mut().cast(); // evil? probably + let mut extension_names = std::slice::from_raw_parts( + device_create_info.pp_enabled_extension_names, + device_create_info.enabled_extension_count as _, + ) + .to_vec(); + + #[cfg(target_os = "windows")] + extension_names.push(ash::extensions::khr::ExternalMemoryWin32::name().as_ptr()); + + #[cfg(target_os = "windows")] + extension_names.push(ash::extensions::khr::ExternalSemaphoreWin32::name().as_ptr()); + + for e in &extension_names { + let e = CStr::from_ptr(*e).to_str().unwrap(); + trace!("Application requested Vulkan extension: {e}") + } + + device_create_info.enabled_extension_count = extension_names.len() as _; + device_create_info.pp_enabled_extension_names = extension_names.as_ptr(); + + trace!("Physical device: {physical_device:?}"); + trace!("Create info: {device_create_info:?}"); + + let device = instance + .create_device(physical_device, device_create_info, None) + .unwrap(); + + *vulkan_device = device.handle().as_raw() as *const _; + + *vulkan_result = vk::Result::SUCCESS.as_raw(); + Result::SUCCESS +} + +pub unsafe extern "system" fn get_vulkan_physical_device( + _instance: Instance, + _system_id: SystemId, + _vk_instance: VkInstance, + _vk_physical_device: *mut VkPhysicalDevice, +) -> Result { + trace!("get_vulkan_physical_device"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn get_vulkan_graphics_requirements( + _instance: Instance, + _system_id: SystemId, + graphics_requirements: *mut GraphicsRequirementsVulkanKHR, +) -> Result { + trace!("get_vulkan_graphics_requirements"); + *graphics_requirements = GraphicsRequirementsVulkanKHR { + ty: GraphicsRequirementsVulkanKHR::TYPE, + next: ptr::null_mut(), + min_api_version_supported: Version::new(1, 1, 0), + max_api_version_supported: Version::new(1, 3, 0), + }; + Result::SUCCESS +} + +pub unsafe extern "system" fn get_instance_properties( + _instance: Instance, + _instance_properties: *mut InstanceProperties, +) -> Result { + trace!("get_instance_properties"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn enumerate_environment_blend_modes( + _instance: Instance, + _system_id: SystemId, + _view_configuration_type: ViewConfigurationType, + _environment_blend_mode_capacity_input: u32, + _environment_blend_mode_count_output: *mut u32, + _environment_blend_modes: *mut EnvironmentBlendMode, +) -> Result { + trace!("enumerate_environment_blend_modes"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn create_session( + _instance: Instance, + create_info: *const SessionCreateInfo, + session: *mut Session, +) -> Result { + trace!("create_session"); + *session = Session::from_raw(rand::random()); + let _ = SESSION.set(*session); // TODO: I'm not sure if it should be an error to create a new session again + let graphics_binding = &*((*create_info).next as *const GraphicsBindingVulkanKHR); + let (entry, instance) = PARTIAL_VULKAN.take().unwrap(); + let physical_device = vk::PhysicalDevice::from_raw(graphics_binding.physical_device as u64); + let device = ash::Device::load(instance.fp_v1_0(), transmute(graphics_binding.device)); + let queue_family_index = graphics_binding.queue_family_index; + + // It's probably fine if the vulkan context already exists + let _ = VULKAN_CONTEXT.set(VulkanContext::new_with_niche_use_case( + entry, + instance, + physical_device, + device, + queue_family_index, + )); + + Result::SUCCESS +} + +pub unsafe extern "system" fn create_action_set( + _instance: Instance, + create_info: *const ActionSetCreateInfo, + action_set: *mut ActionSet, +) -> Result { + trace!("create_action_set"); + let name = CStr::from_ptr((*create_info).action_set_name.as_ptr()); + trace!("Creating action set with name {name:?}"); + *action_set = ActionSet::from_raw(rand::random()); + Result::SUCCESS +} + +pub unsafe extern "system" fn create_action( + _action_set: ActionSet, + _create_info: *const ActionCreateInfo, + action_out: *mut Action, +) -> Result { + trace!("create_action"); + *action_out = Action::from_raw(rand::random()); + Result::SUCCESS +} + +pub unsafe extern "system" fn suggest_interaction_profile_bindings( + _instance: Instance, + suggested_bindings: *const InteractionProfileSuggestedBinding, +) -> Result { + trace!("suggest_interaction_profile_bindings"); + let suggested_bindings = *suggested_bindings; + + let bindings = std::slice::from_raw_parts( + suggested_bindings.suggested_bindings, + suggested_bindings.count_suggested_bindings as _, + ); + + let bindings_map = BINDINGS.get_mut().unwrap(); + + for binding in bindings { + bindings_map.insert(binding.binding, binding.action); + } + + Result::SUCCESS +} + +pub unsafe extern "system" fn string_to_path( + _instance: Instance, + path_string: *const c_char, + path_out: *mut Path, +) -> Result { + trace!("string_to_path"); + match CStr::from_ptr(path_string).to_str() { + Ok(path_string) => { + let path = Path::from_raw(rand::random()); + trace!("Adding ({path_string}, {path:?}) to path map"); + STRING_TO_PATH + .get_mut() + .unwrap() + .insert(path_string.to_string(), path); + PATH_TO_STRING + .get_mut() + .unwrap() + .insert(path, path_string.to_string()); + *path_out = path; + Result::SUCCESS + } + Err(_) => Result::ERROR_VALIDATION_FAILURE, + } +} + +pub unsafe extern "system" fn attach_action_sets( + _session: Session, + _attach_info: *const SessionActionSetsAttachInfo, +) -> Result { + trace!("attach_action_sets"); + Result::SUCCESS +} + +// TODO: Handle aim pose. +pub unsafe extern "system" fn create_action_space( + _session: Session, + create_info: *const ActionSpaceCreateInfo, + space_out: *mut Space, +) -> Result { + trace!("create_action_space"); + let path_string = PATH_TO_STRING + .get() + .unwrap() + .get(&(*create_info).subaction_path) + .map(|s| s.as_str()); + let space = Space::from_raw(rand::random()); + let spaces = SPACES.get_mut().unwrap(); + + match path_string { + Some("/user/hand/left") => { + let mut space_state = SpaceState::new("Left Hand"); + space_state.position = Vector3f { + x: -0.20, + y: 1.4, + z: -0.50, + }; + space_state.orientation = Quaternionf { + x: 0.707, + y: 0., + z: 0., + w: 0.707, + }; + trace!("Created left hand space: {space_state:?}, {space:?}"); + spaces.insert(space.into_raw(), space_state); + } + Some("/user/hand/right") => { + let mut space_state = SpaceState::new("Right Hand"); + space_state.orientation = Quaternionf { + x: 0.707, + y: 0., + z: 0., + w: 0.707, + }; + space_state.position = Vector3f { + x: 0.20, + y: 1.4, + z: -0.50, + }; + trace!("Created right hand space: {space_state:?}, {space:?}"); + spaces.insert(space.into_raw(), space_state); + } + Some(path) => { + let space_state = SpaceState::new(path); + trace!("Created new space: {space_state:?}, {space:?}"); + spaces.insert(space.into_raw(), space_state); + } + _ => return Result::ERROR_PATH_INVALID, + }; + + *space_out = space; + Result::SUCCESS +} + +pub unsafe extern "system" fn create_reference_space( + _session: Session, + create_info: *const ReferenceSpaceCreateInfo, + out_space: *mut Space, +) -> Result { + trace!("create_reference_space"); + let create_info = *create_info; + + // Our "reference space" is Stage with no rotation + let (reference_space, mut space_state) = if create_info.reference_space_type + == ReferenceSpaceType::STAGE + && create_info.pose_in_reference_space.orientation.w != 1.0 + { + // Magic value + (Space::from_raw(0), SpaceState::new("Stage")) + } else { + (Space::from_raw(rand::random()), SpaceState::new("View")) + }; + + space_state.position = create_info.pose_in_reference_space.position; + space_state.orientation = create_info.pose_in_reference_space.orientation; + + SPACES + .get_mut() + .unwrap() + .insert(reference_space.into_raw(), space_state); + + *out_space = reference_space; + Result::SUCCESS +} + +pub unsafe extern "system" fn poll_event( + _instance: Instance, + event_data: *mut EventDataBuffer, +) -> Result { + let next_state = match SESSION_STATE { + SessionState::UNKNOWN => Some(SessionState::IDLE), + SessionState::IDLE => Some(SessionState::READY), + SessionState::READY => Some(SessionState::SYNCHRONIZED), + SessionState::SYNCHRONIZED => Some(SessionState::VISIBLE), + SessionState::VISIBLE => Some(SessionState::FOCUSED), + _ => None, + }; + + if let Some(next_state) = next_state { + SESSION_STATE = next_state; + let event_data = event_data as *mut EventDataSessionStateChanged; + *event_data = EventDataSessionStateChanged { + ty: StructureType::EVENT_DATA_SESSION_STATE_CHANGED, + next: ptr::null(), + session: *SESSION.get().unwrap(), + state: next_state, + time: now(), + }; + return Result::SUCCESS; + } + + Result::EVENT_UNAVAILABLE +} + +pub unsafe extern "system" fn begin_session( + session: Session, + _begin_info: *const SessionBeginInfo, +) -> Result { + trace!("begin_session"); + debug!("Beginning session: {session:?}"); + Result::SUCCESS +} +pub unsafe extern "system" fn wait_frame( + _session: Session, + _frame_wait_info: *const FrameWaitInfo, + frame_state: *mut FrameState, +) -> Result { + trace!("wait_frame"); + + // This is a bit of a hack, but if we're not in the FOCUSED state, we'll be sending `wait_frame` before + // `locate_views` which will annoy the editor. + if SESSION_STATE != SessionState::FOCUSED { + *frame_state = FrameState { + ty: StructureType::FRAME_STATE, + next: null_mut(), + predicted_display_time: now(), + predicted_display_period: openxr_sys::Duration::from_nanos(1), + should_render: false.into(), + }; + + return Result::SUCCESS; + } + + let client = EDITOR_CLIENT.get_mut().unwrap(); + client.request(&requests::WaitFrame).unwrap(); + + *frame_state = FrameState { + ty: StructureType::FRAME_STATE, + next: null_mut(), + predicted_display_time: now(), + predicted_display_period: openxr_sys::Duration::from_nanos(1), + should_render: true.into(), + }; + + Result::SUCCESS +} + +pub unsafe extern "system" fn begin_frame( + _session: Session, + _frame_begin_info: *const FrameBeginInfo, +) -> Result { + trace!("begin_frame"); + Result::SUCCESS +} + +pub unsafe extern "system" fn enumerate_view_configuration_views( + _instance: Instance, + _system_id: SystemId, + _view_configuration_type: ViewConfigurationType, + view_capacity_input: u32, + view_count_output: *mut u32, + views: *mut ViewConfigurationView, +) -> Result { + trace!("enumerate_view_configuration_views"); + let client = EDITOR_CLIENT.get_mut().unwrap(); + if view_capacity_input == 0 { + let view_count = client.request(&requests::GetViewCount {}).unwrap(); + trace!("Received view count from server {view_count}"); + *view_count_output = view_count; + SWAPCHAIN_IMAGE_COUNT = view_count; + return Result::SUCCESS; + } + + let view_configuration = client.request(&requests::GetViewConfiguration {}).unwrap(); + + set_array( + view_capacity_input, + view_count_output, + views, + [ViewConfigurationView { + ty: StructureType::VIEW_CONFIGURATION_VIEW, + next: null_mut(), + recommended_image_rect_width: view_configuration.width, + max_image_rect_height: view_configuration.height, + recommended_swapchain_sample_count: 1, + max_swapchain_sample_count: 1, + max_image_rect_width: view_configuration.width, + recommended_image_rect_height: view_configuration.height, + }; 3], + ); + + Result::SUCCESS +} + +pub unsafe extern "system" fn create_xr_swapchain( + _session: Session, + _create_info: *const SwapchainCreateInfo, + swapchain: *mut Swapchain, +) -> Result { + trace!("create_swapchain"); + *swapchain = Swapchain::from_raw(rand::random()); + Result::SUCCESS +} + +pub unsafe extern "system" fn enumerate_swapchain_images( + _swapchain: Swapchain, + image_capacity_input: u32, + image_count_output: *mut u32, + images: *mut SwapchainImageBaseHeader, +) -> Result { + trace!("enumerate_swapchain_images"); + if image_capacity_input == 0 { + *image_count_output = SWAPCHAIN_IMAGE_COUNT; + return Result::SUCCESS; + } + + let client = EDITOR_CLIENT.get_mut().unwrap(); + + trace!("Requesting swapchain info"); + let swapchain_info = client.request(&requests::GetSwapchainInfo {}).unwrap(); + trace!("Got swapchain info {swapchain_info:?}"); + + trace!("Requesting swapchain image handles.."); + let swapchain_image_handles = client + .request_vec(&requests::GetSwapchainImages {}) + .unwrap(); + trace!("Got swapchain image handles {swapchain_image_handles:?}"); + + trace!("Requesting semaphore handles.."); + let semaphore_handles = client + .request_vec(&requests::GetSwapchainSemaphores {}) + .unwrap(); + trace!("Got semaphore handles {semaphore_handles:?}"); + + let swapchain_images = create_swapchain_images(swapchain_image_handles, swapchain_info); + trace!("Created swapchain images {swapchain_images:?}"); + + let _ = SWAPCHAIN_SEMAPHORES.set(create_swapchain_semaphores(semaphore_handles)); + let _ = SWAPCHAIN_IMAGES.set(swapchain_images.clone()); + + let output_images = std::slice::from_raw_parts_mut( + images as *mut SwapchainImageVulkanKHR, + SWAPCHAIN_IMAGE_COUNT as _, + ); + for (output_image, swapchain_image) in output_images.iter_mut().zip(swapchain_images.iter()) { + *output_image = SwapchainImageVulkanKHR { + ty: StructureType::SWAPCHAIN_IMAGE_VULKAN_KHR, + next: null_mut(), + image: swapchain_image.as_raw(), + }; + } + + Result::SUCCESS +} + +pub unsafe extern "system" fn acquire_swapchain_image( + _swapchain: Swapchain, + _acquire_info: *const SwapchainImageAcquireInfo, + index: *mut u32, +) -> Result { + trace!("acquire_swapchain_image"); + // This is a bit of a hack, but if we're not in the FOCUSED state, we'll be sending `acquire_swapchain_image` before + // `locate_views` which will annoy the editor. + if SESSION_STATE != SessionState::FOCUSED { + *index = 0; + return Result::SUCCESS; + } + + let client = EDITOR_CLIENT.get_mut().unwrap(); + *index = client.request(&AcquireSwapchainImage).unwrap(); + Result::SUCCESS +} + +pub unsafe extern "system" fn wait_swapchain_image( + _swapchain: Swapchain, + _wait_info: *const SwapchainImageWaitInfo, +) -> Result { + trace!("wait_swapchain_image"); + Result::SUCCESS +} + +pub unsafe extern "system" fn dummy() -> Result { + error!("[HOTHAM_OPENXR_CLIENT] Uh oh, dummy called"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn locate_space( + space: Space, + _base_space: Space, + _time: Time, + location_out: *mut SpaceLocation, +) -> Result { + trace!("locate_space"); + match SPACES.get().unwrap().get(&space.into_raw()) { + Some(space_state) => { + let pose = Posef { + position: space_state.position, + orientation: space_state.orientation, + }; + *location_out = SpaceLocation { + ty: StructureType::SPACE_LOCATION, + next: null_mut(), + location_flags: SpaceLocationFlags::ORIENTATION_TRACKED + | SpaceLocationFlags::POSITION_VALID + | SpaceLocationFlags::ORIENTATION_VALID, + pose, + }; + Result::SUCCESS + } + None => Result::ERROR_HANDLE_INVALID, + } +} +pub unsafe extern "system" fn get_action_state_pose( + _session: Session, + _get_info: *const ActionStateGetInfo, + state: *mut ActionStatePose, +) -> Result { + trace!("get_action_state_pose"); + *state = ActionStatePose { + ty: StructureType::ACTION_STATE_POSE, + next: ptr::null_mut(), + is_active: TRUE, + }; + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn sync_actions( + _session: Session, + _sync_info: *const ActionsSyncInfo, +) -> Result { + trace!("sync_actions"); + Result::SUCCESS +} + +pub unsafe extern "system" fn locate_views( + _session: Session, + _view_locate_info: *const ViewLocateInfo, + view_state: *mut ViewState, + view_capacity_input: u32, + view_count_output: *mut u32, + views: *mut View, +) -> Result { + trace!("locate_views"); + + // To avoid hitting the editor twice, early return + if view_capacity_input == 0 { + *view_count_output = 2; + return Result::SUCCESS; + } + + let editor_client = EDITOR_CLIENT.get_mut().unwrap(); + let pose = editor_client.request(&LocateView).unwrap(); + + let view = View { + ty: StructureType::VIEW, + next: null_mut(), + pose, + fov: Fovf { + angle_down: -CAMERA_FIELD_OF_VIEW / 2., + angle_up: CAMERA_FIELD_OF_VIEW / 2., + angle_left: -CAMERA_FIELD_OF_VIEW / 2., + angle_right: CAMERA_FIELD_OF_VIEW / 2., + }, + }; + set_array(view_capacity_input, view_count_output, views, [view; 2]); + *view_state = ViewState { + ty: StructureType::VIEW_STATE, + next: null_mut(), + view_state_flags: ViewStateFlags::ORIENTATION_VALID | ViewStateFlags::POSITION_VALID, + }; + + Result::SUCCESS +} + +pub unsafe extern "system" fn release_swapchain_image( + _swapchain: Swapchain, + _release_info: *const SwapchainImageReleaseInfo, +) -> Result { + trace!("release_swapchain_images"); + Result::SUCCESS +} + +pub unsafe extern "system" fn end_frame( + _session: Session, + _frame_end_info: *const FrameEndInfo, +) -> Result { + trace!("end_frame"); + EDITOR_CLIENT.get_mut().unwrap().request(&EndFrame).unwrap(); + Result::SUCCESS +} + +pub unsafe extern "system" fn request_exit_session(_session: Session) -> Result { + trace!("request_exit_session"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_space(_space: Space) -> Result { + trace!("destroy_space"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_action(_action: Action) -> Result { + trace!("destroy_actions"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_action_set(_action_set: ActionSet) -> Result { + trace!("destroy_action_set"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_swapchain(_swapchain: Swapchain) -> Result { + trace!("destroy_swapchain"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_session(_session: Session) -> Result { + trace!("destroy_session"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn destroy_instance(_instance: Instance) -> Result { + trace!("destroy_instance"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn enumerate_view_configurations( + _instance: Instance, + _system_id: SystemId, + _view_configuration_type_capacity_input: u32, + _view_configuration_type_count_output: *mut u32, + _view_configuration_types: *mut ViewConfigurationType, +) -> Result { + trace!("enumerate_view_configurations"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn enumerate_reference_spaces( + _session: Session, + space_capacity_input: u32, + space_count_output: *mut u32, + spaces: *mut ReferenceSpaceType, +) -> Result { + trace!("enumerate_reference_spaces"); + *space_count_output = 1; + if space_capacity_input == 0 { + return Result::ERROR_FEATURE_UNSUPPORTED; + } + + let spaces = slice::from_raw_parts_mut(spaces, 1); + spaces[0] = ReferenceSpaceType::STAGE; + + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn get_system_properties( + _instance: Instance, + _system_id: SystemId, + _properties: *mut SystemProperties, +) -> Result { + trace!("get_system_properties"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn enumerate_swapchain_formats( + _session: Session, + _format_capacity_input: u32, + _format_count_output: *mut u32, + _formats: *mut i64, +) -> Result { + trace!("enumerate_swapchain_formats"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn get_action_state_float( + _session: Session, + _get_info: *const ActionStateGetInfo, + state: *mut ActionStateFloat, +) -> Result { + trace!("get_action_state_float"); + *state = ActionStateFloat { + ty: StructureType::ACTION_STATE_FLOAT, + next: ptr::null_mut(), + current_state: 0.0, + changed_since_last_sync: FALSE, + last_change_time: openxr_sys::Time::from_nanos(0), + is_active: TRUE, + }; + Result::SUCCESS +} + +pub unsafe extern "system" fn end_session(_session: Session) -> Result { + trace!("end_session"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn get_action_state_boolean( + _session: Session, + get_info: *const ActionStateGetInfo, + action_state: *mut ActionStateBoolean, +) -> Result { + trace!("get_action_state_boolean"); + let current_state = ACTION_STATE.get().unwrap().get_boolean((*get_info).action); + *action_state = ActionStateBoolean { + ty: StructureType::ACTION_STATE_BOOLEAN, + next: ptr::null_mut(), + current_state, + changed_since_last_sync: FALSE, + last_change_time: openxr_sys::Time::from_nanos(0), + is_active: TRUE, + }; + Result::SUCCESS +} + +pub unsafe extern "system" fn apply_haptic_feedback( + _session: Session, + _haptic_action_info: *const HapticActionInfo, + _haptic_feedback: *const HapticBaseHeader, +) -> Result { + trace!("apply_haptic_feedback"); + /* explicit no-op, could possibly be extended with controller support in future if winit ever + * provides such APIs */ + Result::SUCCESS +} + +pub unsafe extern "system" fn get_vulkan_instance_extensions( + _instance: Instance, + _system_id: SystemId, + _buffer_capacity_input: u32, + _buffer_count_output: *mut u32, + _buffer: *mut c_char, +) -> Result { + trace!("get_vulkan_instance_extensions"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +pub unsafe extern "system" fn get_vulkan_device_extensions( + _instance: Instance, + _system_id: SystemId, + _buffer_capacity_input: u32, + _buffer_count_output: *mut u32, + _buffer: *mut c_char, +) -> Result { + trace!("get_vulkan_device_extensions"); + Result::ERROR_FEATURE_UNSUPPORTED +} + +fn str_to_fixed_bytes(string: &str) -> [i8; 128] { + let mut name = [0i8; 128]; + string + .bytes() + .zip(name.iter_mut()) + .for_each(|(b, ptr)| *ptr = b as i8); + name +} + +unsafe fn set_array( + input_count: u32, + output_count: *mut u32, + array_ptr: *mut T, + data: [T; COUNT], +) { + if input_count == 0 { + *output_count = data.len() as _; + return; + } + + // There's probably some clever way to do this without copying, but whatever + let slice = slice::from_raw_parts_mut(array_ptr, COUNT); + slice.copy_from_slice(&data); +} + +fn create_swapchain_images( + handles: Vec, + swapchain_info: SwapchainInfo, +) -> Vec { + let vulkan_context = VULKAN_CONTEXT.get().unwrap(); + let device = &vulkan_context.device; + + handles + .into_iter() + .map(|handle| unsafe { + trace!("Creating image.."); + let handle_type = vk::ExternalMemoryHandleTypeFlags::OPAQUE_WIN32_KMT; + + let mut external_memory_image_create_info = + vk::ExternalMemoryImageCreateInfo::builder().handle_types(handle_type); + let image = device + .create_image( + &vk::ImageCreateInfo { + image_type: vk::ImageType::TYPE_2D, + format: swapchain_info.format, + extent: swapchain_info.resolution.into(), + mip_levels: 1, + array_layers: 2, + samples: vk::SampleCountFlags::TYPE_1, + tiling: vk::ImageTiling::OPTIMAL, + usage: vk::ImageUsageFlags::COLOR_ATTACHMENT, + sharing_mode: vk::SharingMode::EXCLUSIVE, + p_next: &mut external_memory_image_create_info as *mut _ as *mut _, + ..Default::default() + }, + None, + ) + .unwrap(); + trace!("Allocating image memory.."); + let requirements = device.get_image_memory_requirements(image); + let mut external_memory_allocate_info = vk::ImportMemoryWin32HandleInfoKHR::builder() + .handle(handle) + .handle_type(handle_type); + let memory = device + .allocate_memory( + &vk::MemoryAllocateInfo::builder() + .allocation_size(requirements.size) + .push_next(&mut external_memory_allocate_info), + None, + ) + .unwrap(); + trace!("Done, allocating.."); + device.bind_image_memory(image, memory, 0).unwrap(); + image + }) + .collect() +} + +fn create_swapchain_semaphores(handles: Vec) -> Vec { + let vulkan_context = VULKAN_CONTEXT.get().unwrap(); + let device = &vulkan_context.device; + let external_semaphore = ash::extensions::khr::ExternalSemaphoreWin32::new( + &vulkan_context.instance, + &vulkan_context.device, + ); + let handle_type = vk::ExternalSemaphoreHandleTypeFlags::OPAQUE_WIN32_KMT; + + handles + .iter() + .map(|h| unsafe { + let mut external_semaphore_info = + vk::ExportSemaphoreCreateInfo::builder().handle_types(handle_type); + let semaphore = device + .create_semaphore( + &vk::SemaphoreCreateInfo::builder().push_next(&mut external_semaphore_info), + None, + ) + .unwrap(); + + external_semaphore + .import_semaphore_win32_handle( + &vk::ImportSemaphoreWin32HandleInfoKHR::builder() + .handle(*h) + .semaphore(semaphore) + .handle_type(handle_type), + ) + .unwrap(); + + semaphore + }) + .collect() +} + +fn now() -> openxr_sys::Time { + openxr_sys::Time::from_nanos( + (std::time::Instant::now() - *CLOCK.get().unwrap()).as_nanos() as _, + ) +} diff --git a/hotham-openxr-client/src/lib.rs b/hotham-openxr-client/src/lib.rs new file mode 100644 index 00000000..ca861f99 --- /dev/null +++ b/hotham-openxr-client/src/lib.rs @@ -0,0 +1,158 @@ +#![allow(clippy::missing_safety_doc)] + +mod action_state; +mod client; +mod space_state; + +use crate::client::*; +use openxr_sys::{loader, pfn, Instance, Result}; +use std::ffi::c_char; + +type DummyFn = unsafe extern "system" fn() -> Result; + +pub unsafe extern "system" fn get_instance_proc_addr( + _instance: Instance, + name: *const c_char, + function: *mut Option, +) -> Result { + use std::{ffi::CStr, intrinsics::transmute}; + + let name = CStr::from_ptr(name); + let name = name.to_bytes(); + if name == b"xrGetInstanceProcAddr" { + *function = transmute::(get_instance_proc_addr); + } else if name == b"xrEnumerateInstanceExtensionProperties" { + *function = transmute::( + enumerate_instance_extension_properties, + ); + } else if name == b"xrCreateInstance" { + *function = transmute::(create_instance); + } else if name == b"xrCreateVulkanInstanceKHR" { + *function = transmute::(create_vulkan_instance); + } else if name == b"xrCreateVulkanDeviceKHR" { + *function = transmute::(create_vulkan_device); + } else if name == b"xrGetVulkanGraphicsDevice2KHR" { + *function = transmute::(get_vulkan_graphics_device_2); + } else if name == b"xrGetInstanceProperties" { + *function = transmute::(get_instance_properties); + } else if name == b"xrGetVulkanGraphicsRequirements2KHR" { + *function = transmute::( + get_vulkan_graphics_requirements, + ); + } else if name == b"xrGetVulkanGraphicsDeviceKHR" { + *function = transmute::(get_vulkan_physical_device); + } else if name == b"xrGetVulkanGraphicsRequirementsKHR" { + *function = + transmute::(get_vulkan_graphics_requirements); + } else if name == b"xrGetVulkanInstanceExtensionsKHR" { + *function = + transmute::(get_vulkan_instance_extensions); + } else if name == b"xrGetVulkanDeviceExtensionsKHR" { + *function = transmute::(get_vulkan_device_extensions); + } else if name == b"xrEnumerateEnvironmentBlendModes" { + *function = + transmute::(enumerate_environment_blend_modes); + } else if name == b"xrGetSystem" { + *function = transmute::(get_system); + } else if name == b"xrCreateSession" { + *function = transmute::(create_session); + } else if name == b"xrCreateActionSet" { + *function = transmute::(create_action_set); + } else if name == b"xrCreateAction" { + *function = transmute::(create_action); + } else if name == b"xrSuggestInteractionProfileBindings" { + *function = transmute::( + suggest_interaction_profile_bindings, + ); + } else if name == b"xrStringToPath" { + *function = transmute::(string_to_path); + } else if name == b"xrAttachSessionActionSets" { + *function = transmute::(attach_action_sets); + } else if name == b"xrCreateActionSpace" { + *function = transmute::(create_action_space); + } else if name == b"xrCreateReferenceSpace" { + *function = transmute::(create_reference_space); + } else if name == b"xrPollEvent" { + *function = transmute::(poll_event); + } else if name == b"xrBeginSession" { + *function = transmute::(begin_session); + } else if name == b"xrWaitFrame" { + *function = transmute::(wait_frame); + } else if name == b"xrBeginFrame" { + *function = transmute::(begin_frame); + } else if name == b"xrEnumerateViewConfigurationViews" { + *function = transmute::( + enumerate_view_configuration_views, + ); + } else if name == b"xrCreateSwapchain" { + *function = transmute::(create_xr_swapchain); + } else if name == b"xrEnumerateSwapchainImages" { + *function = transmute::(enumerate_swapchain_images); + } else if name == b"xrAcquireSwapchainImage" { + *function = transmute::(acquire_swapchain_image); + } else if name == b"xrWaitSwapchainImage" { + *function = transmute::(wait_swapchain_image); + } else if name == b"xrSyncActions" { + *function = transmute::(sync_actions); + } else if name == b"xrLocateSpace" { + *function = transmute::(locate_space); + } else if name == b"xrGetActionStatePose" { + *function = transmute::(get_action_state_pose); + } else if name == b"xrLocateViews" { + *function = transmute::(locate_views); + } else if name == b"xrReleaseSwapchainImage" { + *function = transmute::(release_swapchain_image); + } else if name == b"xrEndFrame" { + *function = transmute::(end_frame); + } else if name == b"xrRequestExitSession" { + *function = transmute::(request_exit_session); + } else if name == b"xrDestroySpace" { + *function = transmute::(destroy_space); + } else if name == b"xrDestroyAction" { + *function = transmute::(destroy_action); + } else if name == b"xrDestroyActionSet" { + *function = transmute::(destroy_action_set); + } else if name == b"xrDestroySwapchain" { + *function = transmute::(destroy_swapchain); + } else if name == b"xrDestroySession" { + *function = transmute::(destroy_session); + } else if name == b"xrDestroyInstance" { + *function = transmute::(destroy_instance); + } else if name == b"xrEnumerateViewConfigurations" { + *function = transmute::(enumerate_view_configurations); + } else if name == b"xrEnumerateReferenceSpaces" { + *function = transmute::(enumerate_reference_spaces); + } else if name == b"xrGetSystemProperties" { + *function = transmute::(get_system_properties); + } else if name == b"xrEnumerateSwapchainFormats" { + *function = transmute::(enumerate_swapchain_formats); + } else if name == b"xrGetActionStateFloat" { + *function = transmute::(get_action_state_float); + } else if name == b"xrGetActionStateBoolean" { + *function = transmute::(get_action_state_boolean); + } else if name == b"xrApplyHapticFeedback" { + *function = transmute::(apply_haptic_feedback); + } else if name == b"xrEndSession" { + *function = transmute::(end_session); + } else { + let _name = String::from_utf8_unchecked(name.to_vec()); + unsafe extern "system" fn bang() -> Result { + panic!("UNIMPLEMENTED FUNCTION!"); + } + *function = transmute::(bang); + } + Result::SUCCESS +} + +#[no_mangle] +pub unsafe extern "system" fn xrNegotiateLoaderRuntimeInterface( + _loader_info: *const loader::XrNegotiateLoaderInfo, + runtime_request: *mut loader::XrNegotiateRuntimeRequest, +) -> Result { + let runtime_request = &mut *runtime_request; + runtime_request.runtime_interface_version = 1; + runtime_request.runtime_api_version = openxr_sys::CURRENT_API_VERSION; + runtime_request.get_instance_proc_addr = Some(get_instance_proc_addr); + + Result::SUCCESS +} diff --git a/hotham-openxr-client/src/space_state.rs b/hotham-openxr-client/src/space_state.rs new file mode 100644 index 00000000..d839a398 --- /dev/null +++ b/hotham-openxr-client/src/space_state.rs @@ -0,0 +1,41 @@ +use core::fmt::Debug; +use openxr_sys::{Quaternionf, Vector3f}; + +#[derive(Clone)] +pub struct SpaceState { + pub name: String, + pub position: Vector3f, + pub orientation: Quaternionf, +} + +impl SpaceState { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + position: Default::default(), + orientation: Quaternionf::IDENTITY, + } + } +} + +impl Debug for SpaceState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SpaceState") + .field("name", &self.name) + .field( + "position", + &format!( + "x: {}, y: {}, z: {}", + self.position.x, self.position.y, self.position.z + ), + ) + .field( + "orientation", + &format!( + "x: {}, y: {}, z: {}, w: {}", + self.orientation.x, self.orientation.y, self.orientation.z, self.orientation.w + ), + ) + .finish() + } +} diff --git a/hotham-openxr-client/test.ps1 b/hotham-openxr-client/test.ps1 new file mode 100644 index 00000000..9fdfad98 --- /dev/null +++ b/hotham-openxr-client/test.ps1 @@ -0,0 +1,11 @@ +Write-Output "All your OpenXR are belong to crab" +${Env:RUST_BACKTRACE} = 1 + +Write-Output "Starting editor.." +Start-Job -ScriptBlock { cargo run --bin hotham-editor } + +Write-Output "Sleeping.." +Start-Sleep -Seconds 5 + +Write-Output "Starting game.." +cargo run --release --bin hotham_simple_scene_example --features editor diff --git a/hotham-simulator/src/simulator.rs b/hotham-simulator/src/simulator.rs index aba73048..873c2ad1 100644 --- a/hotham-simulator/src/simulator.rs +++ b/hotham-simulator/src/simulator.rs @@ -173,6 +173,7 @@ pub unsafe extern "system" fn create_vulkan_instance( let mut state = STATE.lock().unwrap(); state.vulkan_entry.replace(entry); + println!("[HOTHAM_SIMULATOR] Setting Vulkan instance.."); state.vulkan_instance.replace(ash_instance); Result::SUCCESS } diff --git a/hotham/Cargo.toml b/hotham/Cargo.toml index f8258932..4de27dbc 100644 --- a/hotham/Cargo.toml +++ b/hotham/Cargo.toml @@ -40,6 +40,9 @@ thiserror = "1.0" tokio = {version = "1.0.1", default-features = false, features = ["rt"]} vk-shader-macros = "0.2.8" +[features] +editor = [] + [target.'cfg(not(any(target_os = "macos", target_os = "ios")))'.dev-dependencies] renderdoc = "0.10" diff --git a/hotham/src/contexts/vulkan_context.rs b/hotham/src/contexts/vulkan_context.rs index 022994e6..6524e988 100644 --- a/hotham/src/contexts/vulkan_context.rs +++ b/hotham/src/contexts/vulkan_context.rs @@ -75,7 +75,15 @@ impl VulkanContext { .engine_version(1) .build(); - let create_info = vk::InstanceCreateInfo::builder().application_info(&app_info); + #[cfg(not(debug_assertions))] + let instance_extensions = vec![]; + + #[cfg(debug_assertions)] + let instance_extensions = vec![vk::ExtDebugUtilsFn::name().as_ptr()]; + + let create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&instance_extensions); let instance_handle = unsafe { xr_instance.create_vulkan_instance( @@ -100,6 +108,7 @@ impl VulkanContext { }; // Seems fine. + #[cfg(target_os = "android")] let enabled_extensions = [ "VK_EXT_astc_decode_mode", "VK_EXT_descriptor_indexing", @@ -107,6 +116,10 @@ impl VulkanContext { ] .map(|s| CString::new(s).unwrap().into_raw() as *const c_char); + #[cfg(not(target_os = "android"))] + let enabled_extensions = ["VK_EXT_descriptor_indexing", "VK_KHR_shader_float16_int8"] + .map(|s| CString::new(s).unwrap().into_raw() as *const c_char); + let mut descriptor_indexing_features = vk::PhysicalDeviceDescriptorIndexingFeatures::builder() .shader_sampled_image_array_non_uniform_indexing(true) diff --git a/hotham/src/contexts/xr_context/mod.rs b/hotham/src/contexts/xr_context/mod.rs index fc10eb4b..18531694 100644 --- a/hotham/src/contexts/xr_context/mod.rs +++ b/hotham/src/contexts/xr_context/mod.rs @@ -255,7 +255,7 @@ impl XrContext { } } -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", feature = "editor"))] pub(crate) fn create_vulkan_context( xr_instance: &xr::Instance, system: xr::SystemId, @@ -272,7 +272,7 @@ pub(crate) fn create_vulkan_context( Ok(vulkan_context) } -#[cfg(not(target_os = "android"))] +#[cfg(all(not(target_os = "android"), not(feature = "editor")))] fn create_vulkan_context( xr_instance: &xr::Instance, system: xr::SystemId, @@ -463,7 +463,11 @@ fn enable_xr_extensions(required_extensions: &mut xr::ExtensionSet) { #[cfg(not(target_os = "android"))] fn enable_xr_extensions(required_extensions: &mut xr::ExtensionSet) { - required_extensions.khr_vulkan_enable = true; + if cfg!(feature = "editor") { + required_extensions.khr_vulkan_enable2 = true; + } else { + required_extensions.khr_vulkan_enable = true; + } } #[cfg(target_os = "windows")] diff --git a/hotham/src/rendering/material.rs b/hotham/src/rendering/material.rs index d435b6c0..1b267aef 100644 --- a/hotham/src/rendering/material.rs +++ b/hotham/src/rendering/material.rs @@ -162,7 +162,7 @@ impl Material { } } - /// Create a simple, unlit, white coloured material. + /// Create a simple, unlit, white colored material. pub fn unlit_white() -> Material { Material { packed_flags_and_base_texture_id: MaterialFlags::UNLIT_WORKFLOW.bits,