Intial Commit of starworld

This commit is contained in:
MayaTheShy
2025-11-08 13:39:53 -05:00
commit 83313e2f63
14 changed files with 4443 additions and 0 deletions

47
CMakeLists.txt Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

27
bridge/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}