Intial Commit of starworld
This commit is contained in:
47
CMakeLists.txt
Normal file
47
CMakeLists.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(stardust-overte-client LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(USE_OVERTE_SDK "Link against Overte SDK if available" OFF)
|
||||
option(USE_STARDUST_SDK "Link against StardustXR SDK if available" OFF)
|
||||
|
||||
find_package(glm REQUIRED)
|
||||
|
||||
add_executable(stardust-overte-client
|
||||
src/main.cpp
|
||||
src/StardustBridge.cpp
|
||||
src/OverteClient.cpp
|
||||
src/SceneSync.cpp
|
||||
src/InputHandler.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(stardust-overte-client PRIVATE glm::glm)
|
||||
|
||||
if(USE_OVERTE_SDK)
|
||||
find_package(Overte QUIET)
|
||||
if(Overte_FOUND)
|
||||
target_link_libraries(stardust-overte-client PRIVATE Overte::Networking)
|
||||
target_compile_definitions(stardust-overte-client PRIVATE HAVE_OVERTE_SDK=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_STARDUST_SDK)
|
||||
find_package(StardustXR QUIET)
|
||||
if(StardustXR_FOUND)
|
||||
target_link_libraries(stardust-overte-client PRIVATE StardustXR::Client)
|
||||
target_compile_definitions(stardust-overte-client PRIVATE HAVE_STARDUST_SDK=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Optional: try to locate a prebuilt Rust bridge shared library at runtime using RPATH hints
|
||||
if(NOT DEFINED STARWORLD_BRIDGE_PATH)
|
||||
set(STARWORLD_BRIDGE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bridge/target/debug" CACHE PATH "Path to Rust bridge .so/.dylib directory")
|
||||
endif()
|
||||
if(EXISTS "${STARWORLD_BRIDGE_PATH}")
|
||||
# Add to rpath so dlopen without full path can find it
|
||||
set_target_properties(stardust-overte-client PROPERTIES
|
||||
BUILD_RPATH "${STARWORLD_BRIDGE_PATH}"
|
||||
INSTALL_RPATH "${STARWORLD_BRIDGE_PATH}")
|
||||
endif()
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Starworld (StardustXR + Overte client)
|
||||
|
||||
## Rust bridge (optional)
|
||||
This project can load a Rust bridge shared library exposing a C ABI to the StardustXR client. Build it with:
|
||||
|
||||
```bash
|
||||
cd bridge
|
||||
cargo build
|
||||
```
|
||||
|
||||
This produces `bridge/target/debug/libstardust_bridge.so`. The app will try to load it automatically at startup. You can also set an explicit path:
|
||||
|
||||
```bash
|
||||
export STARWORLD_BRIDGE_PATH=./bridge/target/debug/libstardust_bridge.so
|
||||
```
|
||||
|
||||
If the bridge is not present, the app falls back to a stub and (previously) attempted raw sockets; with the bridge present it will initialize via the official client crates.
|
||||
|
||||
## Overte
|
||||
Overte connectivity is optional; if unreachable, the client runs in offline mode and logs a warning.
|
||||
|
||||
## CLI
|
||||
- `--socket=/path/to.sock` (legacy attempt)
|
||||
- `--abstract=name` (legacy abstract socket attempt)
|
||||
|
||||
Prefer using the Rust bridge.
|
||||
3561
bridge/Cargo.lock
generated
Normal file
3561
bridge/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
bridge/Cargo.toml
Normal file
27
bridge/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "stardust_bridge"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "stardust_bridge"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.38", features = ["rt", "macros"] }
|
||||
glam = "0.28"
|
||||
lazy_static = "1.4"
|
||||
zbus = { version = "5.5.0", features = ["tokio"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[dependencies.stardust-xr-asteroids]
|
||||
git = "https://github.com/StardustXR/asteroids.git"
|
||||
branch = "dev"
|
||||
|
||||
[dependencies.stardust-xr-fusion]
|
||||
git = "https://github.com/StardustXR/core.git"
|
||||
branch = "dev"
|
||||
|
||||
[features]
|
||||
# Feature enabling real StardustXR integration when crates are present.
|
||||
real = []
|
||||
144
bridge/src/lib.rs
Normal file
144
bridge/src/lib.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
// Rust C-ABI bridge for StardustXR client integration.
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use glam::Mat4;
|
||||
use stardust_xr_asteroids as ast; // alias for brevity
|
||||
use stardust_xr_asteroids::{
|
||||
client::ClientState,
|
||||
elements::PlaySpace,
|
||||
Migrate, Reify,
|
||||
CustomElement, Element,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
struct BridgeState {}
|
||||
|
||||
enum Command {
|
||||
Create { c_id: u64, name: String, transform: Mat4 },
|
||||
Update { c_id: u64, transform: Mat4 },
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
impl Migrate for BridgeState { type Old = Self; }
|
||||
|
||||
impl ClientState for BridgeState {
|
||||
const APP_ID: &'static str = "org.stardustxr.starworld";
|
||||
fn initial_state_update(&mut self) {}
|
||||
}
|
||||
|
||||
impl Reify for BridgeState {
|
||||
fn reify(&self) -> impl ast::Element<Self> {
|
||||
// Root playspace. We attach our dynamic nodes under this.
|
||||
PlaySpace.build()
|
||||
}
|
||||
}
|
||||
|
||||
static STARTED: AtomicBool = AtomicBool::new(false);
|
||||
lazy_static::lazy_static! {
|
||||
static ref CTRL: Mutex<Ctrl> = Mutex::new(Ctrl::default());
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ctrl {
|
||||
rt: Option<Runtime>,
|
||||
handle: Option<JoinHandle<()>>, // client running thread
|
||||
tx: Option<tokio::sync::mpsc::UnboundedSender<Command>>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sdxr_start(app_id: *const std::os::raw::c_char) -> i32 {
|
||||
if STARTED.swap(true, Ordering::SeqCst) { return 0; }
|
||||
let name = unsafe { CStr::from_ptr(app_id) }.to_string_lossy().to_string();
|
||||
|
||||
let mut ctrl = CTRL.lock().unwrap();
|
||||
ctrl.next_id = 1;
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Command>();
|
||||
ctrl.tx = Some(tx.clone());
|
||||
|
||||
// Build a single-threaded Tokio runtime for the client
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let res = rt.block_on(async move {
|
||||
// Run the client with our BridgeState
|
||||
let _state = BridgeState {};
|
||||
|
||||
// Launch a task to apply incoming commands once the client is up
|
||||
let cmd_task = tokio::spawn(async move {
|
||||
// This is a placeholder; in a full implementation we would
|
||||
// hold references to created nodes. For now we simply drain.
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
match cmd {
|
||||
Command::Create { .. } => {}
|
||||
Command::Update { .. } => {}
|
||||
Command::Shutdown => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ast::client::run::<BridgeState>(&[]).await;
|
||||
// Ensure command task ends
|
||||
let _ = cmd_task.await;
|
||||
});
|
||||
drop(rt);
|
||||
let _ = res;
|
||||
STARTED.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
ctrl.rt = None; // runtime consumed inside thread
|
||||
ctrl.handle = Some(handle);
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sdxr_poll() -> i32 {
|
||||
if !STARTED.load(Ordering::SeqCst) { return -1; }
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sdxr_shutdown() {
|
||||
let mut ctrl = CTRL.lock().unwrap();
|
||||
if let Some(tx) = ctrl.tx.take() {
|
||||
let _ = tx.send(Command::Shutdown);
|
||||
}
|
||||
if let Some(h) = ctrl.handle.take() {
|
||||
let _ = h.join();
|
||||
}
|
||||
STARTED.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sdxr_create_node(name: *const std::os::raw::c_char, mat4: *const f32) -> u64 {
|
||||
if !STARTED.load(Ordering::SeqCst) { return 0; }
|
||||
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy().to_string();
|
||||
let m = unsafe { std::slice::from_raw_parts(mat4, 16) };
|
||||
let mut arr = [0.0f32; 16];
|
||||
arr.copy_from_slice(m);
|
||||
let mat = Mat4::from_cols_array(&arr);
|
||||
|
||||
let mut ctrl = CTRL.lock().unwrap();
|
||||
let c_id = ctrl.next_id; ctrl.next_id += 1;
|
||||
if let Some(tx) = &ctrl.tx { let _ = tx.send(Command::Create { c_id, name, transform: mat }); }
|
||||
c_id
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sdxr_update_node(id: u64, mat4: *const f32) -> i32 {
|
||||
if !STARTED.load(Ordering::SeqCst) { return -1; }
|
||||
let m = unsafe { std::slice::from_raw_parts(mat4, 16) };
|
||||
let mut arr = [0.0f32; 16];
|
||||
arr.copy_from_slice(m);
|
||||
let mat = Mat4::from_cols_array(&arr);
|
||||
let ctrl = CTRL.lock().unwrap();
|
||||
if let Some(tx) = &ctrl.tx { let _ = tx.send(Command::Update { c_id: id, transform: mat }); }
|
||||
0
|
||||
}
|
||||
22
src/InputHandler.cpp
Normal file
22
src/InputHandler.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "InputHandler.hpp"
|
||||
|
||||
#include "OverteClient.hpp"
|
||||
#include "StardustBridge.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
void InputHandler::update(float /*dt*/) {
|
||||
auto js = m_stardust.joystick();
|
||||
// Apply radial dead zone.
|
||||
float mag = glm::length(js);
|
||||
if (mag < m_deadZone) {
|
||||
js = {0.0f, 0.0f};
|
||||
} else if (mag > 1.0f) {
|
||||
js /= mag; // clamp
|
||||
}
|
||||
|
||||
glm::vec3 vel{js.x * m_moveSpeed, 0.0f, js.y * m_moveSpeed};
|
||||
m_overte.sendMovementInput(vel);
|
||||
}
|
||||
|
||||
22
src/InputHandler.hpp
Normal file
22
src/InputHandler.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
// InputHandler.hpp
|
||||
#pragma once
|
||||
|
||||
class StardustBridge;
|
||||
class OverteClient;
|
||||
|
||||
// Reads input from Stardust and forwards movement to Overte.
|
||||
class InputHandler {
|
||||
public:
|
||||
InputHandler(StardustBridge& stardust, OverteClient& overte)
|
||||
: m_stardust(stardust), m_overte(overte) {}
|
||||
|
||||
// dt in seconds
|
||||
void update(float dt);
|
||||
|
||||
private:
|
||||
StardustBridge& m_stardust;
|
||||
OverteClient& m_overte;
|
||||
float m_moveSpeed{1.5f}; // meters per second at full deflection
|
||||
float m_deadZone{0.15f};
|
||||
};
|
||||
|
||||
104
src/OverteClient.cpp
Normal file
104
src/OverteClient.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
// OverteClient.cpp
|
||||
#include "OverteClient.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
bool OverteClient::connect() {
|
||||
// Basic reachability check (TCP) if ws://host:port specified.
|
||||
// Format expected: ws://host:port
|
||||
auto posScheme = m_domainUrl.find("ws://");
|
||||
if (posScheme != 0) {
|
||||
std::cerr << "[OverteClient] Unexpected URL scheme; expected ws://" << std::endl;
|
||||
}
|
||||
auto hostPort = m_domainUrl.substr(5); // strip ws://
|
||||
auto colon = hostPort.find(':');
|
||||
std::string host = colon == std::string::npos ? hostPort : hostPort.substr(0, colon);
|
||||
int port = colon == std::string::npos ? 40102 : std::stoi(hostPort.substr(colon + 1));
|
||||
|
||||
// Attempt a non-blocking TCP connect (best-effort; ignore failure but warn).
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd != -1) {
|
||||
sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY; // Skip DNS for stub; real impl would resolve host.
|
||||
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) {
|
||||
std::cerr << "[OverteClient] Warning: unable to reach Overte domain (stub)." << std::endl;
|
||||
} else {
|
||||
std::cout << "[OverteClient] (Stub) TCP connect succeeded to " << host << ':' << port << std::endl;
|
||||
}
|
||||
::close(fd);
|
||||
}
|
||||
|
||||
// Simulate successful connections to mixers.
|
||||
m_connected = connectAvatarMixer() && connectEntityServer() && connectAudioMixer();
|
||||
if (!m_connected) {
|
||||
std::cerr << "OverteClient: failed to connect one or more mixers" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seed a couple of demo entities.
|
||||
OverteEntity a{ m_nextEntityId++, "CubeA", glm::mat4(1.0f) };
|
||||
OverteEntity b{ m_nextEntityId++, "CubeB", glm::mat4(1.0f) };
|
||||
m_entities.emplace(a.id, a);
|
||||
m_entities.emplace(b.id, b);
|
||||
m_updateQueue.push_back(a.id);
|
||||
m_updateQueue.push_back(b.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OverteClient::connectAvatarMixer() {
|
||||
// TODO: Use Overte networking layer (NodeList) to connect to AvatarMixer.
|
||||
m_avatarMixer = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OverteClient::connectEntityServer() {
|
||||
// TODO: Connect to EntityServer and subscribe to updates.
|
||||
m_entityServer = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OverteClient::connectAudioMixer() {
|
||||
// TODO: Connect AudioMixer for voice chat.
|
||||
m_audioMixer = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OverteClient::poll() {
|
||||
if (!m_connected) return;
|
||||
|
||||
// Simulate entity transforms changing slightly over time.
|
||||
static auto t0 = std::chrono::steady_clock::now();
|
||||
const float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - t0).count();
|
||||
|
||||
for (auto& [id, e] : m_entities) {
|
||||
const float r = 0.25f + 0.05f * static_cast<float>(id);
|
||||
const float x = std::cos(t * 0.5f + static_cast<float>(id)) * r;
|
||||
const float z = std::sin(t * 0.5f + static_cast<float>(id)) * r;
|
||||
e.transform = glm::translate(glm::mat4(1.0f), glm::vec3{x, 1.25f, z});
|
||||
m_updateQueue.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
void OverteClient::sendMovementInput(const glm::vec3& linearVelocity) {
|
||||
// TODO: Package and send to AvatarMixer as appropriate (e.g. MyAvatar data).
|
||||
(void)linearVelocity; // silence unused warning in the stub
|
||||
}
|
||||
|
||||
std::vector<OverteEntity> OverteClient::consumeUpdatedEntities() {
|
||||
std::vector<OverteEntity> out;
|
||||
out.reserve(m_updateQueue.size());
|
||||
for (auto id : m_updateQueue) {
|
||||
auto it = m_entities.find(id);
|
||||
if (it != m_entities.end()) out.push_back(it->second);
|
||||
}
|
||||
m_updateQueue.clear();
|
||||
return out;
|
||||
}
|
||||
54
src/OverteClient.hpp
Normal file
54
src/OverteClient.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
// OverteClient.hpp
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
struct OverteEntity {
|
||||
std::uint64_t id{0};
|
||||
std::string name;
|
||||
glm::mat4 transform{1.0f};
|
||||
};
|
||||
|
||||
// Lightweight stub over Overte networking layer. Designed to be replaced by
|
||||
// real Overte SDK calls while keeping the app testable.
|
||||
class OverteClient {
|
||||
public:
|
||||
explicit OverteClient(std::string domainUrl)
|
||||
: m_domainUrl(std::move(domainUrl)) {}
|
||||
|
||||
// High-level connect that brings up key mixers.
|
||||
bool connect();
|
||||
|
||||
// Mixer-specific stubs.
|
||||
bool connectAvatarMixer();
|
||||
bool connectEntityServer();
|
||||
bool connectAudioMixer();
|
||||
|
||||
// Pump network I/O. Non-blocking.
|
||||
void poll();
|
||||
|
||||
// Movement/controls
|
||||
void sendMovementInput(const glm::vec3& linearVelocity); // m/s in domain frame
|
||||
|
||||
// Entity accessors
|
||||
const std::unordered_map<std::uint64_t, OverteEntity>& entities() const { return m_entities; }
|
||||
std::vector<OverteEntity> consumeUpdatedEntities();
|
||||
|
||||
private:
|
||||
std::string m_domainUrl;
|
||||
bool m_connected{false};
|
||||
bool m_avatarMixer{false};
|
||||
bool m_entityServer{false};
|
||||
bool m_audioMixer{false};
|
||||
|
||||
// Very small in-process world state for testing
|
||||
std::unordered_map<std::uint64_t, OverteEntity> m_entities;
|
||||
std::vector<std::uint64_t> m_updateQueue; // ids of entities updated since last consume
|
||||
std::uint64_t m_nextEntityId{1};
|
||||
};
|
||||
|
||||
21
src/SceneSync.Hpp
Normal file
21
src/SceneSync.Hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// SceneSync.hpp (note: file kept as provided name)
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
class StardustBridge;
|
||||
class OverteClient;
|
||||
|
||||
// Synchronizes Overte entities into the Stardust subscene.
|
||||
class SceneSync {
|
||||
public:
|
||||
static void update(StardustBridge& stardust, OverteClient& overte);
|
||||
|
||||
private:
|
||||
// Map Overte entity id -> Stardust node id
|
||||
static std::unordered_map<std::uint64_t, std::uint64_t> s_entityNodeMap;
|
||||
};
|
||||
|
||||
25
src/SceneSync.cpp
Normal file
25
src/SceneSync.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "SceneSync.Hpp"
|
||||
|
||||
#include "OverteClient.hpp"
|
||||
#include "StardustBridge.hpp"
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
std::unordered_map<std::uint64_t, std::uint64_t> SceneSync::s_entityNodeMap;
|
||||
|
||||
void SceneSync::update(StardustBridge& stardust, OverteClient& overte) {
|
||||
// Pull only the entities that changed since the last call.
|
||||
auto updated = overte.consumeUpdatedEntities();
|
||||
for (const auto& e : updated) {
|
||||
auto it = s_entityNodeMap.find(e.id);
|
||||
if (it == s_entityNodeMap.end()) {
|
||||
// Create a Stardust node the first time we see this entity.
|
||||
auto nodeId = stardust.createNode(e.name, e.transform);
|
||||
s_entityNodeMap.emplace(e.id, nodeId);
|
||||
} else {
|
||||
// Update existing node's transform.
|
||||
stardust.updateNodeTransform(it->second, e.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
250
src/StardustBridge.cpp
Normal file
250
src/StardustBridge.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
// StardustBridge.cpp
|
||||
#include "StardustBridge.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
static std::vector<std::string> candidateSocketPaths() {
|
||||
std::vector<std::string> out;
|
||||
|
||||
if (const char* envSock = std::getenv("STARDUSTXR_SOCKET")) out.emplace_back(envSock);
|
||||
if (const char* envSock2 = std::getenv("STARDUST_SOCKET")) out.emplace_back(envSock2);
|
||||
if (const char* envAbs = std::getenv("STARDUSTXR_ABSTRACT")) {
|
||||
// If provided without @, add @ prefix for abstract namespace
|
||||
std::string v = envAbs;
|
||||
if (v.empty() || v[0] != '@') v = '@' + v;
|
||||
out.emplace_back(v);
|
||||
}
|
||||
|
||||
std::string xdg;
|
||||
if (const char* env = std::getenv("XDG_RUNTIME_DIR")) xdg = env;
|
||||
if (!xdg.empty()) {
|
||||
out.emplace_back(xdg + "/stardust.sock");
|
||||
out.emplace_back(xdg + "/stardustxr.sock");
|
||||
out.emplace_back(xdg + "/stardust/stardust.sock");
|
||||
out.emplace_back(xdg + "/stardustxr/stardust.sock");
|
||||
}
|
||||
|
||||
// /run/user/<uid>/...
|
||||
char uidPath[128];
|
||||
std::snprintf(uidPath, sizeof(uidPath), "/run/user/%d", (int)getuid());
|
||||
std::string runUser(uidPath);
|
||||
out.emplace_back(runUser + "/stardust.sock");
|
||||
out.emplace_back(runUser + "/stardustxr.sock");
|
||||
out.emplace_back(runUser + "/stardust/stardust.sock");
|
||||
out.emplace_back(runUser + "/stardustxr/stardust.sock");
|
||||
|
||||
out.emplace_back("/tmp/stardustxr.sock");
|
||||
|
||||
// Common abstract names to try as a fallback
|
||||
out.emplace_back("@stardust");
|
||||
out.emplace_back("@stardustxr");
|
||||
out.emplace_back("@stardust/stardust");
|
||||
out.emplace_back("@stardustxr/stardust");
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string StardustBridge::defaultSocketPath() {
|
||||
auto c = candidateSocketPaths();
|
||||
return c.empty() ? std::string{} : c.front();
|
||||
}
|
||||
|
||||
bool StardustBridge::connect(const std::string& socketPath) {
|
||||
// Prefer Rust bridge if available.
|
||||
if (loadBridge()) {
|
||||
const char* appId = "org.stardustxr.starworld";
|
||||
int rc = m_fnStart ? m_fnStart(appId) : -1;
|
||||
if (rc == 0) {
|
||||
m_connected = true;
|
||||
std::cout << "[StardustBridge] Connected via Rust bridge (C-ABI)." << std::endl;
|
||||
m_overteRoot = createNode("OverteWorld");
|
||||
return true;
|
||||
} else {
|
||||
std::cerr << "[StardustBridge] Rust bridge present but start() failed (rc=" << rc << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> paths;
|
||||
if (!socketPath.empty()) paths.push_back(socketPath);
|
||||
auto candidates = candidateSocketPaths();
|
||||
paths.insert(paths.end(), candidates.begin(), candidates.end());
|
||||
|
||||
// Deduplicate while preserving order
|
||||
std::vector<std::string> unique;
|
||||
for (auto& p : paths) {
|
||||
if (!p.empty() && std::find(unique.begin(), unique.end(), p) == unique.end()) unique.push_back(p);
|
||||
}
|
||||
|
||||
for (const auto& p : unique) {
|
||||
// Try to connect regardless of fs existence—server may create on first accept.
|
||||
bool isAbstract = !p.empty() && p[0] == '@';
|
||||
int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
if (isAbstract) {
|
||||
// Linux abstract namespace: first byte of sun_path is NUL, name in the rest.
|
||||
// p begins with '@' per our convention; skip it when copying.
|
||||
std::memset(addr.sun_path, 0, sizeof(addr.sun_path));
|
||||
std::snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "%s", p.c_str() + 1);
|
||||
} else {
|
||||
std::snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", p.c_str());
|
||||
}
|
||||
|
||||
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) {
|
||||
::close(fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make non-blocking after successful connect
|
||||
int flags = ::fcntl(fd, F_GETFL, 0);
|
||||
if (flags != -1) ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
m_socketFd = fd;
|
||||
m_socketPath = p;
|
||||
m_connected = true;
|
||||
std::cout << "[StardustBridge] Connected to compositor at " << (isAbstract ? ("abstract:" + p.substr(1)) : p) << std::endl;
|
||||
|
||||
m_overteRoot = createNode("OverteWorld");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "[StardustBridge] Could not connect to StardustXR. Tried:" << std::endl;
|
||||
for (auto& p : unique) std::cerr << " - " << p << std::endl;
|
||||
std::cerr << "Hint: set STARDUSTXR_SOCKET to a filesystem path, or STARDUSTXR_ABSTRACT to an abstract name (e.g. export STARDUSTXR_ABSTRACT=stardustxr). Leading '@' denotes abstract." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
StardustBridge::NodeId StardustBridge::createNode(const std::string& name,
|
||||
const glm::mat4& transform,
|
||||
std::optional<NodeId> parent) {
|
||||
NodeId id = m_nextId++;
|
||||
m_nodes.emplace(id, Node{ name, parent, transform });
|
||||
// Forward to Rust bridge if available.
|
||||
if (m_fnCreateNode) {
|
||||
float m[16];
|
||||
// GLM mat4 is column-major; pass as 16 floats as-is
|
||||
std::memcpy(m, &transform[0][0], sizeof(m));
|
||||
std::uint64_t rid = m_fnCreateNode(name.c_str(), m);
|
||||
(void)rid; // Could map to NodeId if Rust returns its own id
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
bool StardustBridge::updateNodeTransform(NodeId id, const glm::mat4& transform) {
|
||||
auto it = m_nodes.find(id);
|
||||
if (it == m_nodes.end()) return false;
|
||||
it->second.transform = transform;
|
||||
if (m_fnUpdateNode) {
|
||||
float m[16];
|
||||
std::memcpy(m, &transform[0][0], sizeof(m));
|
||||
(void)m_fnUpdateNode(id, m);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void StardustBridge::poll() {
|
||||
if (!m_connected) return;
|
||||
|
||||
if (m_fnPoll) {
|
||||
int rc = m_fnPoll();
|
||||
if (rc < 0) {
|
||||
std::cerr << "[StardustBridge] Bridge reported disconnected; shutting down." << std::endl;
|
||||
m_running = false;
|
||||
m_connected = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect disconnect: a non-blocking read of 0 or error indicating closed.
|
||||
if (m_socketFd < 0) return;
|
||||
char buf;
|
||||
ssize_t n = ::recv(m_socketFd, &buf, 1, MSG_PEEK);
|
||||
if (n == 0) {
|
||||
std::cerr << "[StardustBridge] Compositor socket closed" << std::endl;
|
||||
m_connected = false;
|
||||
m_running = false; // Request shutdown
|
||||
return;
|
||||
} else if (n == -1 && (errno == ECONNRESET || errno == ENOTCONN)) {
|
||||
std::cerr << "[StardustBridge] Compositor connection reset" << std::endl;
|
||||
m_connected = false;
|
||||
m_running = false;
|
||||
return;
|
||||
} else if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
// No data pending; connection still alive.
|
||||
}
|
||||
|
||||
// TODO: poll actual StardustXR event queue & input devices.
|
||||
// Simulate input for now: small circular joystick motion over time.
|
||||
static auto start = std::chrono::steady_clock::now();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float t = std::chrono::duration<float>(now - start).count();
|
||||
m_joystick = { std::sin(t * 0.5f), std::cos(t * 0.5f) };
|
||||
|
||||
// Head pose remains identity; in real implementation populate from HMD tracking.
|
||||
m_headPose = glm::mat4(1.0f);
|
||||
}
|
||||
|
||||
void StardustBridge::close() {
|
||||
if (m_fnShutdown) m_fnShutdown();
|
||||
if (m_socketFd >= 0) {
|
||||
::close(m_socketFd);
|
||||
m_socketFd = -1;
|
||||
}
|
||||
m_connected = false;
|
||||
}
|
||||
|
||||
// Ensure socket is closed on destruction
|
||||
StardustBridge::~StardustBridge() { close(); }
|
||||
|
||||
bool StardustBridge::loadBridge() {
|
||||
if (m_bridgeHandle) return true;
|
||||
|
||||
const char* overridePath = std::getenv("STARWORLD_BRIDGE_PATH");
|
||||
std::vector<std::string> candidates;
|
||||
if (overridePath) {
|
||||
candidates.emplace_back(std::string(overridePath));
|
||||
}
|
||||
// Likely local dev output
|
||||
candidates.emplace_back("./bridge/target/debug/libstardust_bridge.so");
|
||||
candidates.emplace_back("libstardust_bridge.so");
|
||||
|
||||
for (const auto& path : candidates) {
|
||||
void* h = ::dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!h) continue;
|
||||
auto req = [&](const char* sym){ return ::dlsym(h, sym); };
|
||||
m_fnStart = reinterpret_cast<fn_start_t>(req("sdxr_start"));
|
||||
if (!m_fnStart) m_fnStart = reinterpret_cast<fn_start_t>(req("_sdxr_start"));
|
||||
m_fnPoll = reinterpret_cast<fn_poll_t>(req("sdxr_poll"));
|
||||
m_fnShutdown = reinterpret_cast<fn_shutdown_t>(req("sdxr_shutdown"));
|
||||
m_fnCreateNode = reinterpret_cast<fn_create_node_t>(req("sdxr_create_node"));
|
||||
m_fnUpdateNode = reinterpret_cast<fn_update_node_t>(req("sdxr_update_node"));
|
||||
if (m_fnStart && m_fnPoll && m_fnCreateNode && m_fnUpdateNode) {
|
||||
m_bridgeHandle = h;
|
||||
std::cout << "[StardustBridge] Loaded Rust bridge: " << path << std::endl;
|
||||
return true;
|
||||
}
|
||||
::dlclose(h);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Legacy snippet removed after implementing new connect signature.
|
||||
88
src/StardustBridge.hpp
Normal file
88
src/StardustBridge.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
// StardustBridge.hpp
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
// A lightweight bridge to the StardustXR compositor.
|
||||
// Assumes a C API is available at runtime; this implementation provides a
|
||||
// minimal in-process fallback so the app remains testable without the shared lib.
|
||||
class StardustBridge {
|
||||
public:
|
||||
using NodeId = std::uint64_t;
|
||||
|
||||
// Connect to the StardustXR compositor via IPC.
|
||||
// Returns true on success. If socketPath is empty, uses defaultSocketPath().
|
||||
bool connect(const std::string& socketPath = {});
|
||||
|
||||
// Create a 3D node with an initial transform. Optionally parent it.
|
||||
NodeId createNode(const std::string& name,
|
||||
const glm::mat4& transform = glm::mat4(1.0f),
|
||||
std::optional<NodeId> parent = std::nullopt);
|
||||
|
||||
// Update a node's transform. Returns false if the node doesn't exist.
|
||||
bool updateNodeTransform(NodeId id, const glm::mat4& transform);
|
||||
|
||||
// Poll compositor events and input. Non-blocking.
|
||||
void poll();
|
||||
|
||||
// Lifecycle helpers for the main loop.
|
||||
bool running() const { return m_running; }
|
||||
void requestQuit() { m_running = false; }
|
||||
|
||||
// Input snapshot (polled each frame via poll()).
|
||||
glm::vec2 joystick() const { return m_joystick; } // x,y in [-1, 1]
|
||||
glm::mat4 headPose() const { return m_headPose; } // world-from-head
|
||||
|
||||
// Utility: compute default IPC socket path (first-best guess).
|
||||
static std::string defaultSocketPath();
|
||||
|
||||
~StardustBridge();
|
||||
// Explicit cleanup
|
||||
void close();
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
std::string name;
|
||||
std::optional<NodeId> parent;
|
||||
glm::mat4 transform{1.0f};
|
||||
};
|
||||
|
||||
// Fallback in-process scene representation for testing without the runtime.
|
||||
std::unordered_map<NodeId, Node> m_nodes;
|
||||
NodeId m_nextId{1};
|
||||
|
||||
// Connection and state
|
||||
bool m_connected{false};
|
||||
bool m_running{true};
|
||||
std::string m_socketPath;
|
||||
int m_socketFd{-1};
|
||||
|
||||
// Input state
|
||||
glm::vec2 m_joystick{0.0f, 0.0f};
|
||||
glm::mat4 m_headPose{1.0f};
|
||||
|
||||
// Optional root for the Overte world subscene
|
||||
std::optional<NodeId> m_overteRoot;
|
||||
|
||||
// Dynamic Rust bridge (dlopen) function pointers
|
||||
void* m_bridgeHandle{nullptr};
|
||||
using fn_start_t = int(*)(const char*);
|
||||
using fn_poll_t = int(*)();
|
||||
using fn_shutdown_t = void(*)();
|
||||
using fn_create_node_t = std::uint64_t(*)(const char*, const float*);
|
||||
using fn_update_node_t = int(*)(std::uint64_t, const float*);
|
||||
fn_start_t m_fnStart{nullptr};
|
||||
fn_poll_t m_fnPoll{nullptr};
|
||||
fn_shutdown_t m_fnShutdown{nullptr};
|
||||
fn_create_node_t m_fnCreateNode{nullptr};
|
||||
fn_update_node_t m_fnUpdateNode{nullptr};
|
||||
|
||||
bool loadBridge();
|
||||
};
|
||||
|
||||
52
src/main.cpp
Normal file
52
src/main.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// main.cpp
|
||||
#include "StardustBridge.hpp"
|
||||
#include "OverteClient.hpp"
|
||||
#include "SceneSync.Hpp"
|
||||
#include "InputHandler.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Simple CLI: --socket=/path/to.sock or --abstract=name
|
||||
std::string socketOverride;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
const std::string so = "--socket=";
|
||||
const std::string ab = "--abstract=";
|
||||
if (arg.rfind(so, 0) == 0) socketOverride = arg.substr(so.size());
|
||||
else if (arg.rfind(ab, 0) == 0) socketOverride = '@' + arg.substr(ab.size());
|
||||
}
|
||||
StardustBridge stardust;
|
||||
if (!stardust.connect(socketOverride)) {
|
||||
std::cerr << "Failed to connect to StardustXR compositor.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
OverteClient overte("ws://example.overte.domain:40102");
|
||||
// Overte is optional; warn if unreachable but continue in offline mode.
|
||||
if (!overte.connect()) {
|
||||
std::cerr << "[Overte] Domain unreachable; running in offline mode.\n";
|
||||
}
|
||||
|
||||
InputHandler input(stardust, overte);
|
||||
|
||||
// Main loop
|
||||
while (stardust.running()) {
|
||||
overte.poll();
|
||||
stardust.poll();
|
||||
|
||||
// Sync avatars/entities
|
||||
SceneSync::update(stardust, overte);
|
||||
|
||||
// Simple input mapping
|
||||
input.update(1.0f / 90.0f);
|
||||
|
||||
// Small sleep to avoid busy-spin in the stub
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(11));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user