Skip to content

Commit

Permalink
Vulkan based linux port (#604)
Browse files Browse the repository at this point in the history
* Vulkan based linux port

This version replaces the "screen grab" version.

The mechanism is based on frame capture in vrcompositor process, which
are then shared with vrserver process, encoded there and transmitted to
the headset.
A vulkan layer is added to vrcompositor, this layer exposes the
VK_EXT_direct_mode_display, VK_EXT_acquire_xlib_display and some other
related vulkan functions. Functions implemented in the layer add one
display to the list naturally returned by the driver, this display
matches properties in ALVR settings (resolution and refresh rate), and
all functions required by SteamVR are implemented (xlib acquire,
properties enumeration, vsync event, swapchain creation).
The layer then connects to a socket owned by the server, submits the
VkImage creation parameters, and transfers file descriptors
corresponding to the images in the swapchain and associated semaphores.
The, on each frame, a packet is sent on the socket describing the image
that has been submitted.
On server side, vulkan frames are mapped to vaapi surfaces, using
ffmpeg, then converted to a format suitable for encoding, encoded
through vaapi and sent with the usual mechanism.
In order to associate each frame with tracking information, timing
details are requested to the compositor, and tracking history is queried
to find the most suitable tracking index. This mechanism is still
inaccurate and will need significant upgrades.

Co-authored-by: Ron B <me@ronthecookie.me>

* include pose age in timing calculation

* linux: correctly set timestamps for bitrate

* fix uninitialized variable, layer metadata

* use stack inspection in layer to find pose

In order to get the correct pose associated to an image, search the
stack for the variable, then copy it and send to the server.

* Automatically wrap vrcompositor-launcher

This will allow the user to not have to modify `vrenv.sh` which is quite error-prone.

* update vulkan-layer shell.nix

Co-authored-by: Patrick Nicolas <patricknicolas@laposte.net>
Co-authored-by: Ron B <me@ronthecookie.me>
Co-authored-by: zarik5 <riccardo.zaglia5@gmail.com>
  • Loading branch information
4 people committed Apr 22, 2021
1 parent 85e53ec commit 64288f4
Show file tree
Hide file tree
Showing 64 changed files with 5,764 additions and 2,919 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Expand Up @@ -73,13 +73,13 @@ jobs:
- uses: Swatinem/rust-cache@v1

- name: Install dependencies
run: sudo apt install libasound2-dev libgtk-3-dev libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev
run: sudo apt install libasound2-dev libgtk-3-dev libvulkan-dev

- name: Build crates
uses: actions-rs/cargo@v1
with:
command: build
args: -p alvr_xtask -p alvr_server -p alvr_launcher --verbose
args: -p alvr_xtask -p alvr_launcher --verbose

tests:
runs-on: windows-latest
Expand Down
66 changes: 33 additions & 33 deletions alvr/common/build.rs
Expand Up @@ -6,45 +6,45 @@ fn main() {
return;
}

let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
// let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let workspace_dir = project_dir.parent().unwrap().parent().unwrap();
// let workspace_dir = project_dir.parent().unwrap().parent().unwrap();
let ffmpeg_src_dir = project_dir.join("src").join("ffmpeg");

let ffmpeg_dir = workspace_dir.join("deps").join(&target_os).join("ffmpeg");
// let ffmpeg_dir = workspace_dir.join("deps").join(&target_os).join("ffmpeg");

if target_os == "windows" {
println!(
"cargo:rustc-link-search=native={}",
ffmpeg_dir.join("bin").to_string_lossy()
);
}
println!("cargo:rustc-link-lib=avcodec");
println!("cargo:rustc-link-lib=avdevice");
println!("cargo:rustc-link-lib=avfilter");
println!("cargo:rustc-link-lib=avformat");
println!("cargo:rustc-link-lib=avutil");
println!("cargo:rustc-link-lib=postproc");
println!("cargo:rustc-link-lib=swresample");
println!("cargo:rustc-link-lib=swscale");
// if target_os == "windows" {
// println!(
// "cargo:rustc-link-search=native={}",
// ffmpeg_dir.join("bin").to_string_lossy()
// );
// }
// println!("cargo:rustc-link-lib=avcodec");
// println!("cargo:rustc-link-lib=avdevice");
// println!("cargo:rustc-link-lib=avfilter");
// println!("cargo:rustc-link-lib=avformat");
// println!("cargo:rustc-link-lib=avutil");
// println!("cargo:rustc-link-lib=postproc");
// println!("cargo:rustc-link-lib=swresample");
// println!("cargo:rustc-link-lib=swscale");

let mut build = cc::Build::new();
let mut build = build
.cpp(false)
.include(&ffmpeg_src_dir)
.file(ffmpeg_src_dir.join("ffmpeg.c"));
if target_os != "linux" {
build = build.include(ffmpeg_dir.join("include"));
}
build.compile("ffmpeg_module");
// let mut build = cc::Build::new();
// let mut build = build
// .cpp(false)
// .include(&ffmpeg_src_dir)
// .file(ffmpeg_src_dir.join("ffmpeg.c"));
// if target_os != "linux" {
// build = build.include(ffmpeg_dir.join("include"));
// }
// build.compile("ffmpeg_module");

bindgen::builder()
.header(ffmpeg_src_dir.join("ffmpeg.h").to_string_lossy())
.prepend_enum_name(false)
.generate()
.unwrap()
.write_to_file(out_dir.join("ffmpeg_module.rs"))
.unwrap();
// bindgen::builder()
// .header(ffmpeg_src_dir.join("ffmpeg.h").to_string_lossy())
// .prepend_enum_name(false)
// .generate()
// .unwrap()
// .write_to_file(out_dir.join("ffmpeg_module.rs"))
// .unwrap();

println!(
"cargo:rerun-if-changed={}",
Expand Down
20 changes: 16 additions & 4 deletions alvr/common/src/commands.rs
Expand Up @@ -86,7 +86,7 @@ fn get_single_openvr_path(path_type: &str) -> StrResult<PathBuf> {
trace_none!(from_openvr_paths(paths_json).get(0).cloned())
}

fn steamvr_root_dir() -> StrResult<PathBuf> {
pub fn steamvr_root_dir() -> StrResult<PathBuf> {
get_single_openvr_path("runtime")
}

Expand Down Expand Up @@ -122,8 +122,20 @@ pub fn driver_registration(driver_paths: &[PathBuf], register: bool) -> StrResul
save_openvr_paths_json(&openvr_paths_json)
}

fn get_alvr_dir_store_path() -> StrResult<PathBuf> {
if cfg!(target_os = "linux") {
Ok(dirs::runtime_dir()
.ok_or_else(|| "couldn't get runtime_dir")?
.join(ALVR_DIR_STORAGE_FNAME))
} else if cfg!(windows) {
Ok(env::temp_dir().join(ALVR_DIR_STORAGE_FNAME))
} else {
unimplemented!()
}
}

fn get_alvr_dir_from_storage() -> StrResult<PathBuf> {
let alvr_dir_store_path = env::temp_dir().join(ALVR_DIR_STORAGE_FNAME);
let alvr_dir_store_path = get_alvr_dir_store_path()?;
if let Ok(path) = fs::read_to_string(alvr_dir_store_path) {
Ok(PathBuf::from(path))
} else {
Expand All @@ -147,7 +159,7 @@ pub fn get_alvr_dir() -> StrResult<PathBuf> {
}

pub fn store_alvr_dir(alvr_dir: &Path) -> StrResult {
let alvr_dir_store_path = env::temp_dir().join(ALVR_DIR_STORAGE_FNAME);
let alvr_dir_store_path = get_alvr_dir_store_path()?;

trace_err!(fs::write(
alvr_dir_store_path,
Expand All @@ -156,7 +168,7 @@ pub fn store_alvr_dir(alvr_dir: &Path) -> StrResult {
}

pub fn maybe_delete_alvr_dir_storage() {
fs::remove_file(env::temp_dir().join(ALVR_DIR_STORAGE_FNAME)).ok();
fs::remove_file(get_alvr_dir_store_path().unwrap()).ok();
}

fn driver_paths_backup_present() -> bool {
Expand Down
10 changes: 6 additions & 4 deletions alvr/common/src/data/session.rs
Expand Up @@ -114,10 +114,12 @@ impl Default for SessionDesc {
headset_manufacturer_name: "Oculus".into(),
headset_render_model_name: "generic_hmd".into(),
headset_registered_device_type: "oculus/1WMGH000XX0000".into(),
eye_resolution_width: 960,
eye_resolution_height: 1080,
target_eye_resolution_width: 960,
target_eye_resolution_height: 1080,
// avoid realistic resolutions, as on first start, on Linux, it
// could trigger direct mode on an existing monitor
eye_resolution_width: 800,
eye_resolution_height: 900,
target_eye_resolution_width: 800,
target_eye_resolution_height: 900,
seconds_from_vsync_to_photons: 0.005,
adapter_index: 0,
refresh_rate: 60,
Expand Down
4 changes: 2 additions & 2 deletions alvr/common/src/lib.rs
Expand Up @@ -8,8 +8,8 @@ pub mod sockets;

#[cfg(any(windows, target_os = "linux"))]
pub mod commands;
#[cfg(any(windows, target_os = "linux"))]
pub mod ffmpeg;
// #[cfg(any(windows, target_os = "linux"))]
// pub mod ffmpeg;
#[cfg(any(windows, target_os = "linux"))]
pub mod graphics;

Expand Down
8 changes: 8 additions & 0 deletions alvr/launcher/res/wrapper.sh
@@ -0,0 +1,8 @@
#!/usr/bin/env sh

export VK_LAYER_PATH=$(cat $XDG_RUNTIME_DIR/alvr_dir.txt | rev | cut -d'/' -f3- | rev)/alvr/server/cpp/tools/vulkan-layer/build
export VK_INSTANCE_LAYERS=VK_LAYER_ALVR_capture
export DISABLE_VK_LAYER_VALVE_steam_fossilize_1=1
export DISABLE_VK_LAYER_VALVE_steam_overlay_1=1

exec "$0".real "$@"
33 changes: 33 additions & 0 deletions alvr/launcher/src/commands.rs
Expand Up @@ -6,13 +6,18 @@ use alvr_common::{
use serde_json as json;
use std::{
env, fs,
fs::File,
io::prelude::*,
path::PathBuf,
process::Command,
thread,
time::{Duration, Instant},
};
use sysinfo::{ProcessExt, RefreshKind, System, SystemExt};

#[cfg(target_os = "linux")]
use std::os::unix::fs::PermissionsExt;

const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);

#[cfg(windows)]
Expand Down Expand Up @@ -124,6 +129,9 @@ pub fn maybe_register_alvr_driver() -> StrResult {
.filter(|dir| *dir == current_alvr_dir.clone())
.is_some();

#[cfg(target_os = "linux")]
maybe_wrap_vrcompositor_launcher()?;

if !driver_registered {
let paths_backup = match commands::get_registered_drivers() {
Ok(paths) => paths,
Expand All @@ -147,6 +155,31 @@ pub fn maybe_register_alvr_driver() -> StrResult {
Ok(())
}

#[cfg(target_os = "linux")]
pub fn maybe_wrap_vrcompositor_launcher() -> StrResult {
let steamvr_bin_dir = commands::steamvr_root_dir()?.join("bin").join("linux64");
let real_launcher_path = steamvr_bin_dir.join("vrcompositor-launcher.real");
let launcher_path = steamvr_bin_dir.join("vrcompositor-launcher");

if !real_launcher_path.exists() {
fs::rename(&launcher_path, &real_launcher_path)
.map_err(|_| "couldn't rename original vrcompositor-launcher")?;
let mut file =
File::create(launcher_path).map_err(|_| "couldn't create file at launcher_path")?;
file.write_all(include_bytes!("../res/wrapper.sh"))
.map_err(|_| "couldn't write wrapper script to launcher_path")?;
let mut perms = file
.metadata()
.map_err(|_| "couldn't get file metadata")?
.permissions();
perms.set_mode(0o755); // rwxr-xr-x
file.set_permissions(perms)
.map_err(|_| "couldn't set file perms")?;
}

Ok(())
}

pub fn fix_steamvr() {
// If ALVR driver does not start use a more destructive approach: delete openvrpaths.vrpath then recreate it
if let Ok(path) = commands::openvr_source_file_path() {
Expand Down
12 changes: 11 additions & 1 deletion alvr/server/build.rs
Expand Up @@ -32,7 +32,6 @@ fn main() {

let mut build = cc::Build::new();
build
.debug(false) // This is because we cannot link to msvcrtd (see below)
.cpp(true)
.files(source_files_paths)
.flag_if_supported("-isystemcpp/openvr/headers") // silences many warnings from openvr headers
Expand All @@ -43,6 +42,7 @@ fn main() {

#[cfg(windows)]
build
.debug(false) // This is because we cannot link to msvcrtd (see below)
.define("NOMINMAX", None)
.define("_WINSOCKAPI_", None)
.define("_MBCS", None)
Expand All @@ -68,6 +68,16 @@ fn main() {
);
println!("cargo:rustc-link-lib=openvr_api");

if cfg!(target_os = "linux") {
println!("cargo:rustc-link-lib=vulkan");
println!("cargo:rustc-link-lib=avutil");
println!("cargo:rustc-link-lib=avcodec");
println!("cargo:rustc-link-lib=avfilter");

// fail build if there are undefined symbols in final library
println!("cargo:rustc-cdylib-link-arg=-Wl,--no-undefined");
}

for path in cpp_paths {
println!("cargo:rerun-if-changed={}", path.to_string_lossy());
}
Expand Down
4 changes: 3 additions & 1 deletion alvr/server/cpp/alvr_server/OvrHMD.cpp
Expand Up @@ -169,10 +169,10 @@ vr::EVRInitError OvrHmd::Activate(vr::TrackedDeviceIndex_t unObjectId)
#ifdef _WIN32
Info("Using %ls as primary graphics adapter.\n", m_adapterName.c_str());
Info("OSVer: %ls\n", GetWindowsOSVersion().c_str());
#endif

m_VSyncThread = std::make_shared<VSyncThread>(Settings::Instance().m_refreshRate);
m_VSyncThread->Start();
#endif

m_displayComponent = std::make_shared<OvrDisplayComponent>();
#ifdef _WIN32
Expand Down Expand Up @@ -341,6 +341,8 @@ vr::EVRInitError OvrHmd::Activate(vr::TrackedDeviceIndex_t unObjectId)

m_encoder->OnStreamStart();
#else
// This has to be set after initialization is done, because something in vrcompositor is setting it to 90Hz in the meantime
vr::VRProperties()->SetFloatProperty(m_ulPropertyContainer, vr::Prop_DisplayFrequency_Float, static_cast<float>(Settings::Instance().m_refreshRate));
m_encoder = std::make_shared<CEncoder>(m_Listener, m_poseHistory);
m_encoder->Start();
#endif
Expand Down
6 changes: 3 additions & 3 deletions alvr/server/cpp/alvr_server/PoseHistory.cpp
Expand Up @@ -69,10 +69,10 @@ std::optional<PoseHistory::TrackingHistoryFrame> PoseHistory::GetBestPoseMatch(c
std::optional<PoseHistory::TrackingHistoryFrame> PoseHistory::GetPoseAt(uint64_t client_timestamp_us) const
{
std::unique_lock<std::mutex> lock(m_mutex);
for (const auto& pose: m_poseBuffer)
for (auto it = m_poseBuffer.rbegin(), end = m_poseBuffer.rend() ; it != end ; ++it)
{
if (pose.info.clientTime >= client_timestamp_us)
return pose;
if (it->info.clientTime < client_timestamp_us)
return *it;
}
return {};
}
1 change: 1 addition & 0 deletions alvr/server/cpp/alvr_server/PoseHistory.h
Expand Up @@ -17,6 +17,7 @@ class PoseHistory
void OnPoseUpdated(const TrackingInfo &info);

std::optional<TrackingHistoryFrame> GetBestPoseMatch(const vr::HmdMatrix34_t &pose) const;
// Return the most recent pose known at the given timestamp
std::optional<TrackingHistoryFrame> GetPoseAt(uint64_t client_timestamp_us) const;

private:
Expand Down
3 changes: 1 addition & 2 deletions alvr/server/cpp/alvr_server/Settings.cpp
Expand Up @@ -29,7 +29,7 @@ void Settings::Load()
{
try
{
auto sessionFile = std::ifstream(g_alvrDir + "/session.json"s);
auto sessionFile = std::ifstream(g_alvrDir + (std::string)"/session.json");

auto json = std::string(
std::istreambuf_iterator<char>(sessionFile),
Expand Down Expand Up @@ -137,7 +137,6 @@ void Settings::Load()
Info("Render Target: %d %d\n", m_renderWidth, m_renderHeight);
Info("Seconds from Vsync to Photons: %f\n", m_flSecondsFromVsyncToPhotons);
Info("Refresh Rate: %d\n", m_refreshRate);

m_loaded = true;
}
catch (std::exception &e)
Expand Down
4 changes: 2 additions & 2 deletions alvr/server/cpp/alvr_server/Settings.h
Expand Up @@ -36,8 +36,8 @@ class Settings
uint64_t m_DriverTestMode = 0;

int m_refreshRate;
int32_t m_renderWidth;
int32_t m_renderHeight;
uint32_t m_renderWidth;
uint32_t m_renderHeight;
int32_t m_recommendedTargetWidth;
int32_t m_recommendedTargetHeight;

Expand Down

0 comments on commit 64288f4

Please sign in to comment.