File plugin.cpp
File List > boba_immersive > plugin.cpp
Go to the documentation of this file
#include "plugin.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern char** environ;
namespace {
// Versioned, fixed-layout IPC contracts shared with Boba-ILLIXR. These structs
// intentionally avoid C++ containers and Eigen types so the Python side can
// unpack the bytes without depending on the ILLIXR ABI.
constexpr std::uint32_t kInputVersion = 1;
constexpr char kInputMagic[8] = {'I', 'L', 'L', 'I', 'X', 'R', 'I', '1'};
constexpr char kFrameMagic[8] = {'B', 'O', 'B', 'A', 'Q', 'I', 'M', '1'};
constexpr char kOverlayMagic[8] = {'B', 'O', 'B', 'A', 'O', 'V', 'L', '1'};
constexpr char kModalMagic[8] = {'B', 'O', 'B', 'A', 'M', 'O', 'D', '1'};
constexpr std::uint32_t kFlagActive = 1U << 0U;
constexpr std::uint32_t kFlagPositionValid = 1U << 1U;
constexpr std::uint32_t kFlagOrientationValid = 1U << 2U;
constexpr std::uint32_t kFlagPositionTracked = 1U << 3U;
constexpr std::uint32_t kFlagOrientationTracked = 1U << 4U;
constexpr std::uint32_t kFlagPressed = 1U << 1U;
constexpr char kBobaRuntimeEnvironment[] = "boba-cu132";
constexpr std::uint32_t kFrameVersion = 3;
constexpr std::uint32_t kOverlayVersion = 2;
constexpr std::uint32_t kModalVersion = 1;
constexpr std::uint32_t kEyeCount = 2;
constexpr std::uint32_t kChannelCount = 4;
constexpr std::uint32_t kOverlayCommandFloats = 14;
constexpr std::uint32_t kModalVisibleFlag = 1U << 0U;
constexpr std::uint32_t kModalLeftValidFlag = 1U << 1U;
constexpr std::uint32_t kModalRightValidFlag = 1U << 2U;
#pragma pack(push, 1)
struct InputHeader {
char magic[8];
std::uint32_t version;
std::uint32_t byte_count;
std::uint64_t sequence;
std::int64_t xr_sample_time;
};
struct InputEye {
std::uint32_t flags;
std::uint32_t recommended_width;
std::uint32_t recommended_height;
float position[3];
float orientation[4];
float fov[4];
};
struct InputPose {
std::uint32_t flags;
float position[3];
float orientation[4];
};
struct InputButton {
std::uint32_t flags;
float value;
};
struct InputAxis {
std::uint32_t flags;
float x;
float y;
};
struct InputController {
std::uint32_t available_flags;
std::uint32_t interaction_profile;
InputPose grip;
InputPose aim;
InputButton trigger;
InputButton squeeze;
InputButton primary;
InputButton secondary;
InputButton thumbstick_click;
InputAxis thumbstick;
};
struct InputPacket {
InputHeader header;
InputEye eyes[2];
InputController controllers[2];
};
struct SharedFrameHeader {
char magic[8];
std::uint32_t version;
std::uint32_t width;
std::uint32_t height;
std::uint32_t channels;
std::uint32_t frame_bytes;
std::uint32_t slot_count;
std::uint64_t latest_frame_id;
std::uint64_t latest_slot;
std::uint32_t presentation_mode;
std::uint32_t metadata_bytes;
std::uint8_t padding[8];
};
struct SharedFramePoseMetadataSlot {
std::uint64_t frame_id;
std::uint32_t valid_flags;
std::uint32_t reserved0;
float left_position[3];
float left_orientation[4];
float left_fov[4];
float right_position[3];
float right_orientation[4];
float right_fov[4];
std::uint8_t padding[24];
};
struct SharedOverlayHeader {
char magic[8];
std::uint32_t version;
std::uint32_t command_stride_floats;
std::uint32_t max_commands_per_eye;
std::uint64_t latest_overlay_id;
std::uint32_t left_count;
std::uint32_t right_count;
std::uint32_t slot_count;
std::uint8_t padding[24];
};
struct SharedOverlaySlotMetadata {
std::uint64_t frame_id;
std::uint32_t left_count;
std::uint32_t right_count;
std::uint32_t reserved0;
std::uint32_t reserved1;
std::uint8_t padding[8];
};
struct SharedModalHeader {
char magic[8];
std::uint32_t version;
std::uint32_t max_width;
std::uint32_t max_height;
std::uint32_t slot_count;
std::uint64_t latest_modal_id;
std::uint32_t reserved0;
std::uint32_t reserved1;
std::uint8_t padding[24];
};
struct SharedModalSlotMetadata {
std::uint64_t frame_id;
std::uint32_t valid_flags;
std::uint32_t width;
std::uint32_t height;
std::uint32_t reserved0;
float left_quad[8];
float right_quad[8];
float width_m;
float height_m;
std::uint8_t padding[32];
};
#pragma pack(pop)
// Guard every externally consumed layout at compile time. A field change must
// be accompanied by a protocol-version update and the corresponding Boba parser.
static_assert(sizeof(InputHeader) == 32);
static_assert(sizeof(InputEye) == 56);
static_assert(sizeof(InputPose) == 32);
static_assert(sizeof(InputButton) == 8);
static_assert(sizeof(InputAxis) == 12);
static_assert(sizeof(InputController) == 124);
static_assert(sizeof(InputPacket) == 392);
static_assert(sizeof(SharedFrameHeader) == 64);
static_assert(sizeof(SharedFramePoseMetadataSlot) == 128);
static_assert(sizeof(SharedOverlayHeader) == 64);
static_assert(sizeof(SharedOverlaySlotMetadata) == 32);
static_assert(sizeof(SharedModalHeader) == 64);
static_assert(sizeof(SharedModalSlotMetadata) == 128);
template<typename T>
bool read_struct(const std::uint8_t* data, std::size_t size, std::size_t offset, T* value) {
if (data == nullptr || offset > size || sizeof(T) > size - offset) {
return false;
}
std::memcpy(value, data + offset, sizeof(T));
return true;
}
// Translate typed switchboard values into the compact local input protocol.
std::uint32_t pose_flags(const ILLIXR::data_format::quest_controller_pose& pose) {
std::uint32_t flags = 0;
flags |= pose.active ? kFlagActive : 0U;
flags |= pose.position_valid ? kFlagPositionValid : 0U;
flags |= pose.orientation_valid ? kFlagOrientationValid : 0U;
flags |= pose.position_tracked ? kFlagPositionTracked : 0U;
flags |= pose.orientation_tracked ? kFlagOrientationTracked : 0U;
return flags;
}
void copy_pose(const ILLIXR::data_format::quest_controller_pose& source, InputPose* destination) {
destination->flags = pose_flags(source);
destination->position[0] = source.position.x;
destination->position[1] = source.position.y;
destination->position[2] = source.position.z;
destination->orientation[0] = source.orientation.x;
destination->orientation[1] = source.orientation.y;
destination->orientation[2] = source.orientation.z;
destination->orientation[3] = source.orientation.w;
}
void copy_button(const ILLIXR::data_format::quest_controller_button& source, InputButton* destination) {
destination->flags = (source.active ? kFlagActive : 0U) | (source.pressed ? kFlagPressed : 0U);
destination->value = source.value;
}
void copy_controller(const ILLIXR::data_format::quest_hand_controller& source, InputController* destination) {
destination->available_flags = source.available ? kFlagActive : 0U;
destination->interaction_profile = static_cast<std::uint32_t>(source.interaction_profile);
copy_pose(source.grip_pose, &destination->grip);
copy_pose(source.aim_pose, &destination->aim);
copy_button(source.trigger, &destination->trigger);
copy_button(source.squeeze, &destination->squeeze);
copy_button(source.primary, &destination->primary);
copy_button(source.secondary, &destination->secondary);
copy_button(source.thumbstick_click, &destination->thumbstick_click);
destination->thumbstick.flags = source.thumbstick.active ? kFlagActive : 0U;
destination->thumbstick.x = source.thumbstick.value.x();
destination->thumbstick.y = source.thumbstick.value.y();
}
void copy_eye(const ILLIXR::data_format::openxr_eye_view& source, InputEye* destination) {
destination->flags = source.valid ? (kFlagActive | kFlagPositionValid | kFlagOrientationValid) : 0U;
if (source.pose_tracked) {
destination->flags |= kFlagPositionTracked | kFlagOrientationTracked;
}
destination->recommended_width = source.recommended_width;
destination->recommended_height = source.recommended_height;
destination->position[0] = source.position.x;
destination->position[1] = source.position.y;
destination->position[2] = source.position.z;
destination->orientation[0] = source.orientation.x;
destination->orientation[1] = source.orientation.y;
destination->orientation[2] = source.orientation.z;
destination->orientation[3] = source.orientation.w;
destination->fov[0] = source.angle_left;
destination->fov[1] = source.angle_right;
destination->fov[2] = source.angle_up;
destination->fov[3] = source.angle_down;
}
void copy_render_view(const float position[3], const float orientation[4], const float fov[4], bool valid,
ILLIXR::data_format::stereo_render_view* destination) {
destination->valid = valid;
destination->position = {position[0], position[1], position[2]};
destination->orientation = {orientation[0], orientation[1], orientation[2], orientation[3]};
destination->angle_left = fov[0];
destination->angle_right = fov[1];
destination->angle_up = fov[2];
destination->angle_down = fov[3];
}
bool same_magic(const char actual[8], const char expected[8]) {
return std::memcmp(actual, expected, 8) == 0;
}
std::filesystem::path default_boba_install_root() {
if (const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); xdg_data_home != nullptr && *xdg_data_home != '\0') {
return std::filesystem::path{xdg_data_home} / "illixr" / "boba_immersive";
}
if (const char* home = std::getenv("HOME"); home != nullptr && *home != '\0') {
return std::filesystem::path{home} / ".local" / "share" / "illixr" / "boba_immersive";
}
return {};
}
// A direct launcher override is useful during Boba development; normal users
// resolve the setup-managed installation through BOBA_IMMERSIVE_ROOT/XDG data.
std::string resolve_boba_launcher(const std::shared_ptr<ILLIXR::switchboard>& switchboard) {
const std::string launcher_override = switchboard->get_env("BOBA_DEMO_LAUNCHER");
if (!launcher_override.empty()) {
return launcher_override;
}
std::filesystem::path install_root{switchboard->get_env("BOBA_IMMERSIVE_ROOT")};
if (install_root.empty()) {
install_root = default_boba_install_root();
}
if (install_root.empty()) {
return "Boba-ILLIXR/boba_app.sh";
}
// BOBA_IMMERSIVE_ROOT may name the companion checkout directly, or the
// parent directory used by --install-root and the default XDG installation.
if (std::filesystem::is_regular_file(install_root / "boba_app.sh")) {
return (install_root / "boba_app.sh").string();
}
return (install_root / "Boba-ILLIXR" / "boba_app.sh").string();
}
} // namespace
namespace ILLIXR {
// ---- Shared-memory resource ownership -------------------------------------
boba_immersive::mapped_file::~mapped_file() {
reset();
}
void boba_immersive::mapped_file::reset() {
if (data != nullptr) {
munmap(const_cast<std::uint8_t*>(data), size);
data = nullptr;
size = 0;
}
if (fd >= 0) {
close(fd);
fd = -1;
}
}
boba_immersive::boba_immersive(const std::string& name, phonebook* pb)
: plugin{name, pb}
, switchboard_{pb->lookup_impl<switchboard>()}
, clock_{pb->lookup_impl<relative_clock>()}
, stoplight_{pb->lookup_impl<stoplight>()}
, controller_reader_{switchboard_->get_reader<controller_input>("quest_controller")}
, view_reader_{switchboard_->get_reader<view_frame>("openxr_view")}
, stereo_writer_{switchboard_->get_writer<stereo_frame>("stereo_frame")}
, boba_launcher_{resolve_boba_launcher(switchboard_)} {
spdlogger(switchboard_->get_env_char("BOBA_IMMERSIVE_LOG_LEVEL", "info"));
// Only the native/offload profile has a remote Quest process to stop. The
// desktop OpenXR compatibility profile shuts down in this same process.
if (switchboard_->get_env_bool("BOBA_NATIVE_QUEST_STREAM", "false")) {
network::topic_config control_config{};
control_config.serialization_method = network::topic_config::SerializationMethod::PROTOBUF;
control_config.transport_method = network::topic_config::TransportMethod::TCP;
native_client_control_writer_.emplace(
switchboard_->get_network_writer<switchboard::event_wrapper<std::string>>("boba_client_control", control_config));
}
}
boba_immersive::~boba_immersive() {
stop_requested_.store(true);
if (worker_.joinable()) {
worker_.join();
}
}
// ---- Plugin and child-process lifecycle -----------------------------------
void boba_immersive::start() {
plugin::start();
stop_requested_.store(false);
worker_ = std::thread([this]() {
run();
});
}
void boba_immersive::stop() {
stop_requested_.store(true);
if (worker_.joinable()) {
worker_.join();
}
plugin::stop();
}
void boba_immersive::run() {
stoplight_->wait_for_ready();
if (!create_runtime_directory() || !create_input_socket() || !launch_boba()) {
terminate_boba();
cleanup_runtime_directory();
plugin_logger_->error("Boba immersive startup failed; requesting ILLIXR shutdown");
notify_native_client_shutdown();
stoplight_->signal_should_stop();
return;
}
plugin_logger_->info("Boba immersive producer started; input={} output={}", input_socket_path_, frame_path_);
while (!stop_requested_.load() && !stoplight_->check_should_stop()) {
// Detect a voluntary game exit (including Boba's hold-to-exit action)
// before doing more IPC work, then propagate it to the whole pipeline.
int status = 0;
const pid_t wait_result = waitpid(boba_pid_, &status, WNOHANG);
if (wait_result == boba_pid_) {
if (WIFEXITED(status)) {
const int exit_code = WEXITSTATUS(status);
if (exit_code == 0) {
plugin_logger_->info("Boba immersive process exited cleanly");
} else {
plugin_logger_->error("Boba immersive process exited with code {}", exit_code);
}
} else if (WIFSIGNALED(status)) {
plugin_logger_->error("Boba immersive process terminated by signal {}", WTERMSIG(status));
}
boba_pid_ = -1;
plugin_logger_->info("Boba immersive stopped; requesting ILLIXR shutdown");
notify_native_client_shutdown();
stoplight_->signal_should_stop();
break;
}
// Input and output are deliberately non-blocking. Boba and ILLIXR may
// run at different rates, so each side consumes only the newest sample.
send_latest_input();
map_output_files();
publish_latest_frame();
std::this_thread::sleep_for(std::chrono::milliseconds{1});
}
terminate_boba();
notify_native_client_shutdown();
frame_mapping_.reset();
overlay_mapping_.reset();
modal_mapping_.reset();
cleanup_runtime_directory();
}
void boba_immersive::notify_native_client_shutdown() {
if (!native_client_control_writer_.has_value() || native_client_shutdown_sent_) {
return;
}
auto message = native_client_control_writer_->allocate(std::string{"shutdown"});
native_client_control_writer_->put(std::move(message));
native_client_shutdown_sent_ = true;
plugin_logger_->info("Requested native Quest client shutdown");
}
bool boba_immersive::create_runtime_directory() {
std::array<char, 64> template_path{};
const char* prefix = "/tmp/illixr-boba-XXXXXX";
std::copy(prefix, prefix + std::strlen(prefix) + 1, template_path.begin());
char* result = mkdtemp(template_path.data());
if (result == nullptr) {
plugin_logger_->error("mkdtemp failed: {}", std::strerror(errno));
return false;
}
runtime_directory_ = result;
input_socket_path_ = runtime_directory_ + "/input.sock";
frame_path_ = runtime_directory_ + "/stereo.bin";
overlay_path_ = runtime_directory_ + "/overlay.bin";
modal_path_ = runtime_directory_ + "/modal.bin";
return true;
}
bool boba_immersive::create_input_socket() {
input_socket_ = socket(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0);
if (input_socket_ < 0) {
plugin_logger_->error("socket(AF_UNIX) failed: {}", std::strerror(errno));
return false;
}
if (input_socket_path_.size() >= sizeof(sockaddr_un::sun_path)) {
plugin_logger_->error("Boba input socket path is too long: {}", input_socket_path_);
return false;
}
return true;
}
bool boba_immersive::launch_boba() {
if (!std::filesystem::is_regular_file(boba_launcher_)) {
plugin_logger_->error("Boba launcher was not found: {}. Run setup_boba_immersive.sh, set BOBA_IMMERSIVE_ROOT, or set "
"BOBA_DEMO_LAUNCHER to override the launcher directly.",
boba_launcher_);
return false;
}
// Copy the parent environment while replacing any stale bridge variables
// inherited from an earlier run.
std::vector<std::string> environment_storage;
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
const std::string_view value{*entry};
if (value.rfind("BOBA_ILLIXR_INPUT_SOCKET=", 0) == 0 || value.rfind("BOBA_ILLIXR_FRAME_PATH=", 0) == 0 ||
value.rfind("BOBA_ILLIXR_OVERLAY_PATH=", 0) == 0 || value.rfind("BOBA_ILLIXR_MODAL_PATH=", 0) == 0 ||
value.rfind("BOBA_RUNTIME_ENV=", 0) == 0) {
continue;
}
environment_storage.emplace_back(*entry);
}
environment_storage.emplace_back("BOBA_ILLIXR_INPUT_SOCKET=" + input_socket_path_);
environment_storage.emplace_back("BOBA_ILLIXR_FRAME_PATH=" + frame_path_);
environment_storage.emplace_back("BOBA_ILLIXR_OVERLAY_PATH=" + overlay_path_);
environment_storage.emplace_back("BOBA_ILLIXR_MODAL_PATH=" + modal_path_);
environment_storage.emplace_back(std::string{"BOBA_RUNTIME_ENV="} + kBobaRuntimeEnvironment);
std::vector<char*> environment;
environment.reserve(environment_storage.size() + 1);
for (std::string& value : environment_storage) {
environment.push_back(value.data());
}
environment.push_back(nullptr);
std::vector<char*> arguments{
const_cast<char*>("/bin/bash"),
boba_launcher_.data(),
const_cast<char*>("--illixr"),
};
if (switchboard_->get_env_bool("BOBA_NATIVE_QUEST_STREAM", "false")) {
// The upstream spectator view renders a separate desktop camera. Keep
// it optional so native headset delivery does not pay for a third view.
const bool preview = switchboard_->get_env_bool("BOBA_DESKTOP_PREVIEW", "false");
arguments.push_back(const_cast<char*>("--interactive_window_mode"));
arguments.push_back(const_cast<char*>(preview ? "visible" : "hidden"));
plugin_logger_->info("Boba desktop spectator preview: {}", preview ? "visible" : "hidden");
}
arguments.push_back(nullptr);
// Give Boba its own process group because its shell launcher creates Python
// and helper children that must all terminate with the ILLIXR plugin.
posix_spawnattr_t attributes;
if (posix_spawnattr_init(&attributes) != 0) {
plugin_logger_->error("posix_spawnattr_init failed");
return false;
}
short flags = POSIX_SPAWN_SETPGROUP;
posix_spawnattr_setflags(&attributes, flags);
posix_spawnattr_setpgroup(&attributes, 0);
const int result = posix_spawn(&boba_pid_, "/bin/bash", nullptr, &attributes, arguments.data(), environment.data());
posix_spawnattr_destroy(&attributes);
if (result != 0) {
boba_pid_ = -1;
plugin_logger_->error("Unable to launch Boba: {}", std::strerror(result));
return false;
}
plugin_logger_->info("Launched the existing Boba immersive demo (pid={}, launcher={}, conda_env={})", boba_pid_,
boba_launcher_, kBobaRuntimeEnvironment);
return true;
}
void boba_immersive::terminate_boba() {
if (boba_pid_ <= 0) {
return;
}
kill(-boba_pid_, SIGTERM);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{5};
while (std::chrono::steady_clock::now() < deadline) {
int status = 0;
const pid_t result = waitpid(boba_pid_, &status, WNOHANG);
if (result == boba_pid_ || (result < 0 && errno == ECHILD)) {
boba_pid_ = -1;
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds{50});
}
plugin_logger_->warn("Boba did not stop after SIGTERM; sending SIGKILL");
kill(-boba_pid_, SIGKILL);
waitpid(boba_pid_, nullptr, 0);
boba_pid_ = -1;
}
void boba_immersive::cleanup_runtime_directory() {
if (input_socket_ >= 0) {
close(input_socket_);
input_socket_ = -1;
}
for (const std::string* path : {&input_socket_path_, &frame_path_, &overlay_path_, &modal_path_}) {
if (!path->empty()) {
unlink(path->c_str());
}
}
if (!runtime_directory_.empty()) {
rmdir(runtime_directory_.c_str());
}
}
// ---- ILLIXR-to-Boba input bridge ------------------------------------------
bool boba_immersive::send_latest_input() {
const auto controller = controller_reader_.get_ro_nullable();
const auto views = view_reader_.get_ro_nullable();
// Never combine a controller snapshot with views from a different OpenXR
// predicted display time; Boba treats the packet as one coherent sample.
if (controller == nullptr || views == nullptr || controller->sequence != views->sequence) {
return false;
}
if (have_input_sequence_ && views->sequence <= last_input_sequence_) {
return false;
}
InputPacket packet{};
std::memcpy(packet.header.magic, kInputMagic, sizeof(kInputMagic));
packet.header.version = kInputVersion;
packet.header.byte_count = sizeof(packet);
packet.header.sequence = views->sequence;
packet.header.xr_sample_time = views->xr_sample_time;
copy_eye(views->left, &packet.eyes[0]);
copy_eye(views->right, &packet.eyes[1]);
copy_controller(controller->left, &packet.controllers[0]);
copy_controller(controller->right, &packet.controllers[1]);
sockaddr_un destination{};
destination.sun_family = AF_UNIX;
std::memcpy(destination.sun_path, input_socket_path_.c_str(), input_socket_path_.size() + 1);
const ssize_t sent = sendto(input_socket_, &packet, sizeof(packet), MSG_DONTWAIT,
reinterpret_cast<const sockaddr*>(&destination), sizeof(destination));
if (sent == static_cast<ssize_t>(sizeof(packet))) {
last_input_sequence_ = views->sequence;
have_input_sequence_ = true;
return true;
}
if (sent < 0 && errno != ENOENT && errno != ECONNREFUSED && errno != EAGAIN && errno != EWOULDBLOCK) {
plugin_logger_->warn("Unable to send Boba input sample: {}", std::strerror(errno));
}
return false;
}
// ---- Boba-to-ILLIXR shared-ring bridge ------------------------------------
bool boba_immersive::map_if_ready(const std::string& path, mapped_file* mapping) {
if (mapping->data != nullptr) {
return true;
}
const int fd = open(path.c_str(), O_RDONLY);
if (fd < 0) {
return false;
}
struct stat status{};
if (fstat(fd, &status) != 0 || status.st_size <= 0) {
close(fd);
return false;
}
void* data = mmap(nullptr, static_cast<std::size_t>(status.st_size), PROT_READ, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return false;
}
mapping->fd = fd;
mapping->data = static_cast<const std::uint8_t*>(data);
mapping->size = static_cast<std::size_t>(status.st_size);
return true;
}
bool boba_immersive::map_output_files() {
const bool frame_ready = map_if_ready(frame_path_, &frame_mapping_);
map_if_ready(overlay_path_, &overlay_mapping_);
map_if_ready(modal_path_, &modal_mapping_);
return frame_ready;
}
bool boba_immersive::publish_latest_frame() {
if (frame_mapping_.data == nullptr) {
return false;
}
// Snapshot the producer's current slot metadata before constructing any
// switchboard references into the recyclable ring.
SharedFrameHeader header{};
if (!read_struct(frame_mapping_.data, frame_mapping_.size, 0, &header) || !same_magic(header.magic, kFrameMagic) ||
header.version != kFrameVersion || header.channels != kChannelCount || header.slot_count == 0 ||
header.latest_slot >= header.slot_count || header.latest_frame_id == 0 || header.latest_frame_id <= last_frame_id_) {
return false;
}
const std::size_t slot = static_cast<std::size_t>(header.latest_slot);
const std::size_t metadata_offset = sizeof(SharedFrameHeader) + slot * sizeof(SharedFramePoseMetadataSlot);
SharedFramePoseMetadataSlot pose_metadata{};
if (!read_struct(frame_mapping_.data, frame_mapping_.size, metadata_offset, &pose_metadata) ||
pose_metadata.frame_id != header.latest_frame_id) {
return false;
}
const std::uint64_t eye_bytes = static_cast<std::uint64_t>(header.width) * header.height * header.channels;
if (header.frame_bytes != eye_bytes * kEyeCount ||
header.metadata_bytes < header.slot_count * sizeof(SharedFramePoseMetadataSlot)) {
return false;
}
const std::uint64_t slot_offset =
sizeof(SharedFrameHeader) + header.metadata_bytes + static_cast<std::uint64_t>(slot) * header.frame_bytes;
if (slot_offset > frame_mapping_.size || header.frame_bytes > frame_mapping_.size - slot_offset) {
return false;
}
auto output = stereo_writer_.allocate();
output->sequence = header.latest_frame_id;
output->sample_time = clock_->now();
output->source_frame_id = header.latest_frame_id;
output->format = data_format::stereo_pixel_format::rgba8_unorm;
output->origin = data_format::stereo_image_origin::upper_left;
if (header.presentation_mode <= static_cast<std::uint32_t>(data_format::stereo_presentation_mode::head_locked_panel)) {
output->presentation_mode = static_cast<data_format::stereo_presentation_mode>(header.presentation_mode);
}
output->pixel_buffer_path = frame_path_;
output->pixel_generation_offset = metadata_offset;
output->left = {slot_offset, eye_bytes, header.width, header.height, header.width * header.channels};
output->right = {slot_offset + eye_bytes, eye_bytes, header.width, header.height, header.width * header.channels};
copy_render_view(pose_metadata.left_position, pose_metadata.left_orientation, pose_metadata.left_fov,
(pose_metadata.valid_flags & 1U) != 0, &output->left_render_view);
copy_render_view(pose_metadata.right_position, pose_metadata.right_orientation, pose_metadata.right_fov,
(pose_metadata.valid_flags & 2U) != 0, &output->right_render_view);
// Overlay and modal rings are optional. They are attached only when their
// slot generation matches the pixel frame exactly.
SharedOverlayHeader overlay_header{};
const std::size_t overlay_metadata_offset = sizeof(SharedOverlayHeader) + slot * sizeof(SharedOverlaySlotMetadata);
SharedOverlaySlotMetadata overlay_metadata{};
if (read_struct(overlay_mapping_.data, overlay_mapping_.size, 0, &overlay_header) &&
same_magic(overlay_header.magic, kOverlayMagic) && overlay_header.version == kOverlayVersion &&
overlay_header.command_stride_floats == kOverlayCommandFloats && overlay_header.slot_count == header.slot_count &&
read_struct(overlay_mapping_.data, overlay_mapping_.size, overlay_metadata_offset, &overlay_metadata) &&
overlay_metadata.frame_id == header.latest_frame_id) {
const std::uint64_t command_stride_bytes =
static_cast<std::uint64_t>(overlay_header.command_stride_floats) * sizeof(float);
const std::uint64_t eye_stride_bytes =
static_cast<std::uint64_t>(overlay_header.max_commands_per_eye) * command_stride_bytes;
const std::uint64_t payload_offset = sizeof(SharedOverlayHeader) +
static_cast<std::uint64_t>(overlay_header.slot_count) * sizeof(SharedOverlaySlotMetadata) +
static_cast<std::uint64_t>(slot) * kEyeCount * eye_stride_bytes;
if (payload_offset <= overlay_mapping_.size && kEyeCount * eye_stride_bytes <= overlay_mapping_.size - payload_offset) {
output->overlay_buffer_path = overlay_path_;
output->overlay_generation_offset = overlay_metadata_offset;
output->left_overlay_commands = {
payload_offset,
std::min(overlay_metadata.left_count, overlay_header.max_commands_per_eye),
overlay_header.command_stride_floats,
};
output->right_overlay_commands = {
payload_offset + eye_stride_bytes,
std::min(overlay_metadata.right_count, overlay_header.max_commands_per_eye),
overlay_header.command_stride_floats,
};
}
}
SharedModalHeader modal_header{};
const std::size_t modal_metadata_offset = sizeof(SharedModalHeader) + slot * sizeof(SharedModalSlotMetadata);
SharedModalSlotMetadata modal_metadata{};
if (read_struct(modal_mapping_.data, modal_mapping_.size, 0, &modal_header) &&
same_magic(modal_header.magic, kModalMagic) && modal_header.version == kModalVersion &&
modal_header.slot_count == header.slot_count &&
read_struct(modal_mapping_.data, modal_mapping_.size, modal_metadata_offset, &modal_metadata) &&
modal_metadata.frame_id == header.latest_frame_id && modal_metadata.width <= modal_header.max_width &&
modal_metadata.height <= modal_header.max_height) {
const std::uint64_t modal_slot_bytes =
static_cast<std::uint64_t>(modal_header.max_width) * modal_header.max_height * kChannelCount;
const std::uint64_t modal_payload_offset = sizeof(SharedModalHeader) +
static_cast<std::uint64_t>(modal_header.slot_count) * sizeof(SharedModalSlotMetadata) +
static_cast<std::uint64_t>(slot) * modal_slot_bytes;
if (modal_payload_offset <= modal_mapping_.size && modal_slot_bytes <= modal_mapping_.size - modal_payload_offset) {
output->modal_buffer_path = modal_path_;
output->modal_generation_offset = modal_metadata_offset;
output->modal.visible = (modal_metadata.valid_flags & kModalVisibleFlag) != 0;
output->modal.left_valid = (modal_metadata.valid_flags & kModalLeftValidFlag) != 0;
output->modal.right_valid = (modal_metadata.valid_flags & kModalRightValidFlag) != 0;
output->modal.byte_offset = modal_payload_offset;
output->modal.width = modal_metadata.width;
output->modal.height = modal_metadata.height;
output->modal.source_row_stride_bytes = modal_header.max_width * kChannelCount;
for (std::size_t index = 0; index < 4; ++index) {
output->modal.left_quad_pixels[index] = {modal_metadata.left_quad[index * 2],
modal_metadata.left_quad[index * 2 + 1]};
output->modal.right_quad_pixels[index] = {modal_metadata.right_quad[index * 2],
modal_metadata.right_quad[index * 2 + 1]};
}
output->modal.width_m = modal_metadata.width_m;
output->modal.height_m = modal_metadata.height_m;
}
}
// Re-read the generation after collecting all ranges. If Boba recycled the
// slot while we were reading it, drop the descriptor instead of publishing
// a mixture of two frames.
SharedFrameHeader confirm_header{};
SharedFramePoseMetadataSlot confirm_metadata{};
if (!read_struct(frame_mapping_.data, frame_mapping_.size, 0, &confirm_header) ||
!read_struct(frame_mapping_.data, frame_mapping_.size, metadata_offset, &confirm_metadata) ||
confirm_header.latest_frame_id != header.latest_frame_id || confirm_header.latest_slot != header.latest_slot ||
confirm_metadata.frame_id != header.latest_frame_id) {
return false;
}
last_frame_id_ = header.latest_frame_id;
const std::uint32_t published_left_overlays = output->left_overlay_commands.command_count;
const std::uint32_t published_right_overlays = output->right_overlay_commands.command_count;
const bool published_modal = output->modal.visible;
stereo_writer_.put(std::move(output));
if (last_frame_id_ == 1 || last_frame_id_ % 300 == 0) {
plugin_logger_->info("Published Boba stereo_frame id={} size={}x{} overlays={}/{} modal={}", last_frame_id_,
header.width, header.height, published_left_overlays, published_right_overlays, published_modal);
}
return true;
}
} // namespace ILLIXR
using namespace ILLIXR;
PLUGIN_MAIN(boba_immersive)