├── res ├── speaker-256.png └── SourceSansPro-Regular.ttf ├── .gitmodules ├── .gitignore ├── src ├── qml │ ├── HeaderText.qml │ ├── VolumeSlider.qml │ ├── VRComboBox.qml │ └── main.qml ├── strs.h ├── main.cpp ├── vrmanager.h ├── pamanager.h ├── pamanager.cpp └── vrmanager.cpp ├── .clang-format ├── resources.qrc ├── CMakeLists.txt └── README.md /res/speaker-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Supreeeme/vrdio/HEAD/res/speaker-256.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "openvr"] 2 | path = openvr 3 | url = https://github.com/ValveSoftware/OpenVR 4 | -------------------------------------------------------------------------------- /res/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Supreeeme/vrdio/HEAD/res/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .idea 3 | build 4 | debug 5 | vrdio 6 | compile_commands.json 7 | vrdio-launch.sh 8 | vrdio-release* 9 | -------------------------------------------------------------------------------- /src/qml/HeaderText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.0 2 | 3 | Text{ 4 | FontLoader{ 5 | id: localFont 6 | source: "qrc:/SourceSansPro-Regular.ttf" 7 | } 8 | font.family: localFont.name 9 | font.pointSize: 40 10 | color: "white" 11 | } 12 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | AlignOperands: Align 4 | AlignAfterOpenBracket: DontAlign 5 | BreakBeforeBinaryOperators: NonAssignment 6 | IndentWidth: 4 7 | TabWidth: 4 8 | ContinuationIndentWidth: 8 9 | ColumnLimit: 100 10 | IncludeBlocks: Regroup 11 | UseTab: AlignWithSpaces 12 | PointerAlignment: Left 13 | --- 14 | -------------------------------------------------------------------------------- /src/strs.h: -------------------------------------------------------------------------------- 1 | #ifndef STRS_H 2 | #define STRS_H 3 | 4 | #include 5 | namespace strings { 6 | inline constexpr auto app_key = "supreme.vrdio"; 7 | inline constexpr auto overlay_friendly_name = "Audio Control"; 8 | inline auto config_dir_loc = QDir::homePath() + "/.config/vrdio"; 9 | inline auto vrmanifest_loc = config_dir_loc + "/vrdio.vrmanifest"; 10 | inline auto audioconfig_loc = config_dir_loc + "/audioconfig.txt"; 11 | 12 | } // namespace strings 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | src/qml/main.qml 4 | src/qml/VolumeSlider.qml 5 | src/qml/VRComboBox.qml 6 | src/qml/HeaderText.qml 7 | res/SourceSansPro-Regular.ttf 8 | res/speaker-256.png 9 | 10 | 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19.0) 2 | 3 | project(vrdio VERSION 1.0 LANGUAGES CXX) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED True) 7 | set(CMAKE_AUTOMOC ON) 8 | set(CMAKE_AUTORCC ON) 9 | 10 | # put exe in main directory 11 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) 12 | find_package(Qt6 COMPONENTS Core Gui Quick QuickControls2 REQUIRED) 13 | 14 | file(GLOB SOURCES "${PROJECT_SOURCE_DIR}/src/*.cpp" "${PROJECT_SOURCE_DIR}/resources.qrc") 15 | 16 | add_executable(vrdio ${SOURCES}) 17 | target_include_directories(vrdio PRIVATE ${PROJECT_SOURCE_DIR}/src openvr/headers) 18 | target_link_directories(vrdio PRIVATE openvr/lib/linux64) 19 | target_link_libraries(vrdio PRIVATE Qt6::Core Qt6::Quick Qt6::QuickControls2 Qt6::Gui openvr_api vulkan pulse) 20 | 21 | -------------------------------------------------------------------------------- /src/qml/VolumeSlider.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.0 2 | import QtQuick.Controls 6.0 3 | 4 | Slider { 5 | implicitHeight: handle.height + 20 6 | implicitWidth: 400 7 | 8 | background: Rectangle { 9 | x: parent.leftPadding 10 | y: parent.topPadding + parent.availableHeight / 2 - height / 2 11 | width: parent.availableWidth 12 | height: 10 13 | radius: 2 14 | color: "#bdbebf" 15 | 16 | Rectangle { 17 | width: parent.parent.visualPosition * parent.width 18 | height: parent.height 19 | color: "#21be2b" 20 | radius: 2 21 | } 22 | } 23 | 24 | handle: Rectangle { // actually a circle! 25 | x: parent.leftPadding + parent.visualPosition * (parent.availableWidth - width) 26 | y: parent.topPadding + parent.availableHeight / 2 - height / 2 27 | implicitWidth: 26 28 | implicitHeight: 26 29 | width: 80 30 | height: 80 31 | radius: 80 32 | color: parent.pressed ? "#f0f0f0" : "#f6f6f6" 33 | border.color: "#bdbebf" 34 | 35 | Text { 36 | anchors.centerIn: parent 37 | font.pointSize: 15 38 | font.family: parent.parent.font.family 39 | text: parseInt(parent.parent.value) + "%" 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VRdio - Audio Controls in VR for Linux 2 | Control volume, audio device configurations, and set a default audio configuration all from the SteamVR dashboard. 3 | (a screenshot would be great to have here but screenshots unfortunately don't work in steamvr linux valve pls fix) 4 | 5 | ## Dependencies 6 | - SteamVR 7 | - Pulseaudio (or Pulseaudio stand in, such as pipewire-pulse) + libpulse 8 | - Vulkan ICD Loader (`libvulkan1` on Ubuntu, `vulkan-icd-loader` on Arch) 9 | - SDL (`libsdl2-2.0-0` on Ubuntu, `sdl2` on Arch) 10 | - libopengl-dev (Ubuntu) / libglvnd (Arch) 11 | - Qt5/Qt6 (probably already installed - if not, look at [Qt's dependencies](https://doc.qt.io/qt-5/linux-requirements.html)) 12 | 13 | ## How do I get it? 14 | Grab the latest release, place and extract it wherever you like, and double click (or run from terminal) `vrdio-launch.sh` while SteamVR is open. It will be autolaunched next time you run SteamVR. 15 | If you want to uninstall it from SteamVR, run `./vrdio-launch.sh --uninstall`. 16 | 17 | ## Features to come 18 | - [ ] Audio mirroring 19 | 20 | Feel free to request features! 21 | 22 | ## Building 23 | Dependencies: 24 | - libpulse 25 | - libvulkan 26 | - Qt6 libraries (qt6-base, qt6-declarative) 27 | ``` 28 | mkdir build 29 | cd build 30 | cmake .. 31 | make 32 | ``` 33 | This will place the `vrdio` binary in the top level directory. Libraries used for building releases are bundled in the latest release. 34 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "openvr.h" 2 | #include "pamanager.h" 3 | #include "vrmanager.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace vr; 13 | int main(int argc, char* argv[]) { 14 | QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); 15 | QGuiApplication a(argc, argv); 16 | 17 | QCommandLineParser parser; 18 | parser.setApplicationDescription("VR Audio Controls for Linux"); 19 | parser.addHelpOption(); 20 | parser.addOption({"uninstall", "Uninstalls the manifest from Steam."}); 21 | parser.process(a); 22 | 23 | if (parser.isSet("uninstall")) { 24 | VRManager::uninstall(); 25 | return 0; 26 | } 27 | 28 | QQuickRenderControl renderCtrl; 29 | QQuickView w(QUrl(), &renderCtrl); 30 | QVulkanInstance instance; 31 | 32 | // setup vulkan 33 | VRManager ctrl(&w, &renderCtrl); 34 | 35 | // setup pulseaudio 36 | PAManager pulse("VR Audio Control"); 37 | 38 | // expose PAManager to QML 39 | // have to call setContextProperty before setting source, otherwise you get 40 | // annoying errors 41 | w.rootContext()->setContextProperty("pulse", &pulse); 42 | w.setSource(QUrl("qrc:///main.qml")); 43 | 44 | if (!renderCtrl.initialize()) { 45 | throw std::runtime_error("Failed to initialize QQuickRenderControl!"); 46 | } 47 | 48 | ctrl.buildOverlay(); 49 | 50 | return a.exec(); 51 | } 52 | -------------------------------------------------------------------------------- /src/vrmanager.h: -------------------------------------------------------------------------------- 1 | #ifndef VRMANAGER_H 2 | #define VRMANAGER_H 3 | 4 | #include "openvr.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class VRManager : public QObject { 14 | Q_OBJECT 15 | public: 16 | VRManager(QQuickView* window, QQuickRenderControl* rc); 17 | ~VRManager(); 18 | static void uninstall(); 19 | void buildOverlay(); 20 | 21 | public slots: 22 | void prepareSceneGraph(); 23 | void checkRender(); 24 | 25 | private: 26 | // initialization 27 | static void initVR(bool uninstall = false); 28 | void initVulkan(); 29 | void getPhysicalDevice(); 30 | void createLogicalDevice(); 31 | void createImage(); 32 | 33 | // rendering 34 | void createCommandPool(); 35 | VkCommandBuffer beginSingleTimeCommands(); 36 | void endSingleTimeCommands(VkCommandBuffer b); 37 | void transitionImageLayout(VkImageLayout newLayout); 38 | void render(); 39 | 40 | void pollEvents(); 41 | 42 | QTemporaryDir temp_dir; // for icon 43 | 44 | QQuickView* window; 45 | QQuickRenderControl* renderCtrl; 46 | QVulkanInstance instance; 47 | QVulkanFunctions* vkFuncs; 48 | 49 | VkPhysicalDevice physicalDevice; 50 | VkDevice device; 51 | 52 | VkQueue graphicsQueue; 53 | QVulkanDeviceFunctions* devFuncs; 54 | uint32_t graphicsFamily; 55 | 56 | VkImage image; 57 | VkImageLayout curLayout; 58 | VkDeviceMemory imageMem; 59 | 60 | VkCommandPool commandPool; 61 | 62 | vr::VROverlayHandle_t overlay, icon; 63 | int overlayWidth, overlayHeight; 64 | std::unique_ptr checkTimer; 65 | 66 | Qt::MouseButtons activeMouseButtons; 67 | QPointF lastPos; 68 | }; 69 | 70 | #endif // VRMANAGER_H 71 | -------------------------------------------------------------------------------- /src/qml/VRComboBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.0 2 | import QtQuick.Controls 6.0 3 | 4 | ComboBox{ 5 | FontLoader{ 6 | id: localFont 7 | source: "qrc:/SourceSansPro-Regular.ttf" 8 | } 9 | 10 | id: dropdown 11 | implicitHeight: 60 12 | font.family: localFont.name 13 | font.pointSize: 33 14 | 15 | contentItem: Text{ 16 | text: dropdown.displayText 17 | width: parent.width 18 | elide: Text.ElideRight 19 | font: dropdown.font 20 | horizontalAlignment: Text.AlignHCenter 21 | verticalAlignment: Text.AlignVCenter; 22 | } 23 | 24 | popup.contentItem: ListView{ 25 | model: dropdown.popup.visible ? dropdown.delegateModel : null 26 | currentIndex: dropdown.currentIndex 27 | implicitHeight: contentHeight 28 | interactive: false 29 | maximumFlickVelocity: 0.1 30 | 31 | MouseArea{ 32 | id: m 33 | anchors.fill: parent 34 | onWheel: (wheel)=>{ 35 | if (wheel.angleDelta.y > 0) 36 | scrollbar.increase() 37 | else 38 | scrollbar.decrease() 39 | } 40 | acceptedButtons: Qt.NoButton 41 | propagateComposedEvents: true 42 | } 43 | ScrollBar.vertical: ScrollBar{ 44 | id: scrollbar 45 | policy: ScrollBar.AlwaysOn 46 | visible: parent.height < parent.contentHeight 47 | width: 20 48 | stepSize: 0.05 49 | } 50 | } 51 | 52 | delegate: ItemDelegate{ 53 | id: del 54 | width: dropdown.width 55 | text: modelData 56 | font.pointSize: 30 57 | font.bold: (dropdown.currentIndex == index) 58 | font.family: dropdown.font.family 59 | highlighted: (area.containsMouse) 60 | MouseArea{ 61 | id: area 62 | acceptedButtons: Qt.NoButton 63 | anchors.fill: parent 64 | hoverEnabled: true 65 | } 66 | } 67 | 68 | 69 | background: Rectangle{ 70 | color: dropdown.down ? "#e0e0e0" : "#d0d0d0" 71 | radius: 15 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/pamanager.h: -------------------------------------------------------------------------------- 1 | #ifndef PAMANAGER_H 2 | #define PAMANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | // pamanager.h: holds all pulseaudio related actions 11 | 12 | struct Profile { 13 | std::string name; 14 | std::string description; 15 | bool active; 16 | }; 17 | class Card : public QObject { 18 | Q_OBJECT 19 | Q_PROPERTY(QString description MEMBER description CONSTANT) 20 | Q_PROPERTY(QStringList profiles READ getProfileList CONSTANT) 21 | Q_PROPERTY(Profile activeProfile READ getActiveProfile) 22 | Q_PROPERTY(int activeProfileIndex MEMBER activeProfileIndex) 23 | public: 24 | std::string name; // alsa_card_XXXXX... 25 | uint32_t index; 26 | QString description; 27 | std::vector availableProfiles; 28 | 29 | unsigned int activeProfileIndex; 30 | Profile getActiveProfile(); 31 | QStringList getProfileList() const; 32 | signals: 33 | void profilesChanged(); 34 | }; 35 | 36 | class PAManager : public QObject { 37 | Q_OBJECT 38 | Q_PROPERTY(QStringList sinks READ getSinkList NOTIFY sinksChanged) 39 | Q_PROPERTY(QVariantList cards READ getCardList NOTIFY cardsChanged) 40 | Q_PROPERTY(int sinkIndex READ getDefaultSinkIndex NOTIFY newDefaultSink) 41 | public: 42 | explicit PAManager(const char* appName); 43 | ~PAManager(); 44 | 45 | public slots: 46 | int getVolPct(); 47 | QStringList getSinkList(); 48 | QVariantList getCardList(); 49 | void changeVol(int newPct); 50 | void changeSink(int sinkIndex); 51 | void changeCardProfile(Card* card, const QString& profileName); 52 | int getDefaultSinkIndex() const; 53 | bool saveConfig(); 54 | void loadConfig(); 55 | 56 | signals: 57 | void sinksChanged(); 58 | void cardsChanged(); 59 | void newDefaultSink(); 60 | 61 | private: 62 | struct Sink { 63 | std::string name; 64 | std::string description; // user friendly name 65 | uint32_t index; 66 | pa_cvolume volume; 67 | bool operator==(const Sink& other) const; 68 | }; 69 | 70 | std::vector sinkList; 71 | 72 | // cardList is a vector of pointers because QObjects have no copy constructors. 73 | std::vector> cardList; 74 | 75 | pa_threaded_mainloop* mainloop; 76 | pa_context* context; 77 | int defaultSinkIndex; 78 | 79 | static void waitForContext(pa_context* c, void* userdata); 80 | 81 | // (Re)builds sink list 82 | void buildSinkList(); 83 | 84 | void getDefaultSink(); 85 | 86 | void buildCardList(); 87 | 88 | void waitForOpFinish(pa_operation*); 89 | }; 90 | 91 | #endif // PAMANAGER_H 92 | -------------------------------------------------------------------------------- /src/qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.0 2 | import QtQuick.Controls 6.0 3 | 4 | Page { 5 | id: topLevel 6 | width: screenWidth 7 | height: screenHeight 8 | background: Rectangle{ 9 | anchors.fill: parent 10 | gradient: Gradient{ 11 | GradientStop { position: 0.0; color: "#464A4B" } 12 | GradientStop { position: 0.7; color: "#292C32" } 13 | } 14 | } 15 | 16 | property alias currentCardIndex: cardDropdown.currentIndex 17 | property alias currentProfileIndex: profileDropdown.currentIndex 18 | 19 | MouseArea{ 20 | anchors.fill: parent 21 | onPressedChanged: { 22 | if (pressed) parent.focus = true 23 | } 24 | } 25 | 26 | HeaderText{ 27 | id: sinkText 28 | text: "Default Output" 29 | y: 100 30 | 31 | anchors.horizontalCenter: parent.horizontalCenter 32 | } 33 | 34 | VRComboBox{ 35 | id: sinkDropdown 36 | anchors.top: sinkText.bottom 37 | anchors.topMargin: 20 38 | anchors.horizontalCenter: sinkText.horizontalCenter 39 | 40 | width: 1300 41 | model: pulse.sinks 42 | property bool ready: false 43 | onCurrentIndexChanged: { 44 | // avoid changing sink on startup 45 | if (!ready){ 46 | ready = true 47 | return 48 | } 49 | pulse.changeSink(currentIndex) 50 | } 51 | Component.onCompleted:{ 52 | setIndex() 53 | pulse.newDefaultSink.connect(setIndex) 54 | } 55 | 56 | function setIndex(){ 57 | currentIndex = pulse.sinkIndex 58 | } 59 | } 60 | 61 | VolumeSlider { 62 | id: slider 63 | width: sinkDropdown.width 64 | anchors.horizontalCenter: sinkDropdown.horizontalCenter 65 | anchors.top: sinkDropdown.bottom 66 | anchors.bottomMargin: 20 67 | 68 | from: 0 69 | to: 100 70 | 71 | Component.onCompleted: { 72 | pulse.newDefaultSink.connect(volUpdate) 73 | volUpdate() 74 | } 75 | onValueChanged: pulse.changeVol(value) 76 | function volUpdate(){ 77 | value = pulse.getVolPct() 78 | } 79 | } 80 | 81 | Button{ 82 | id: configButton 83 | anchors.top: slider.bottom 84 | anchors.topMargin: 20 85 | height: 50 86 | font.pointSize: 30 87 | anchors.horizontalCenter: slider.horizontalCenter 88 | text: "Save config" 89 | onClicked: { 90 | configFeedback.visible = true 91 | if (pulse.saveConfig()) 92 | configFeedback.text = "Config saved successfully!" 93 | else 94 | configFeedback.text = "Config was not saved due to an error!" 95 | configFeedbackTimer.start() 96 | } 97 | } 98 | HeaderText{ 99 | id: configFeedback 100 | property bool success 101 | anchors.top: configButton.bottom 102 | anchors.topMargin: 20 103 | anchors.horizontalCenter: parent.horizontalCenter 104 | Timer { 105 | id: configFeedbackTimer 106 | interval: 2000 107 | onTriggered: parent.visible = false 108 | } 109 | } 110 | 111 | Rectangle{ 112 | id: separator 113 | width: screenWidth - 40 114 | height: 5 115 | y: screenHeight/2 + 100 116 | anchors.horizontalCenter: parent.horizontalCenter 117 | color: "gray" 118 | } 119 | 120 | HeaderText{ 121 | id: configText 122 | text: "Device Configuration" 123 | anchors.top: separator.bottom 124 | anchors.topMargin: 20 125 | anchors.horizontalCenter: separator.horizontalCenter 126 | } 127 | 128 | HeaderText{ 129 | id: cardText 130 | text: "Device" 131 | anchors.horizontalCenter: cardDropdown.horizontalCenter 132 | anchors.top: configText.bottom 133 | anchors.topMargin: 100 134 | } 135 | 136 | VRComboBox{ 137 | id: cardDropdown 138 | width: 1000 139 | 140 | anchors.top: cardText.bottom 141 | anchors.topMargin: 20 142 | anchors.left: parent.left 143 | anchors.leftMargin: 30 144 | model: { 145 | var l = [] 146 | for (var i = 0; i 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | void PAManager::waitForContext(pa_context* c, void* userdata) { 12 | if (pa_context_get_state(c) != PA_CONTEXT_READY) 13 | return; 14 | 15 | // send signal that context is ready and mainloop can stop waiting 16 | pa_threaded_mainloop_signal(static_cast(userdata), 0); 17 | } 18 | PAManager::PAManager(const char* appName) 19 | : mainloop(nullptr), context(nullptr), defaultSinkIndex(-1) { 20 | // create mainloop 21 | mainloop = pa_threaded_mainloop_new(); 22 | 23 | // create context 24 | pa_mainloop_api* api = pa_threaded_mainloop_get_api(mainloop); 25 | context = pa_context_new(api, appName); 26 | 27 | // set context callback. used to escape wait loop below 28 | // mainloop passed as userdata for signalling 29 | pa_context_set_state_callback(context, &PAManager::waitForContext, mainloop); 30 | 31 | // connect to context 32 | pa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr); 33 | 34 | // start the mainloop 35 | pa_threaded_mainloop_start(mainloop); 36 | 37 | pa_threaded_mainloop_lock(mainloop); 38 | // wait for context to be ready 39 | while (pa_context_get_state(context) != PA_CONTEXT_READY) 40 | pa_threaded_mainloop_wait(mainloop); 41 | pa_threaded_mainloop_unlock(mainloop); 42 | 43 | loadConfig(); 44 | buildSinkList(); 45 | getDefaultSink(); 46 | buildCardList(); 47 | } 48 | 49 | PAManager::~PAManager() { 50 | pa_context_disconnect(context); 51 | pa_threaded_mainloop_stop(mainloop); 52 | pa_threaded_mainloop_free(mainloop); 53 | } 54 | 55 | void PAManager::waitForOpFinish(pa_operation* o) { 56 | pa_threaded_mainloop_lock(mainloop); 57 | while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) 58 | pa_threaded_mainloop_wait(mainloop); 59 | pa_threaded_mainloop_unlock(mainloop); 60 | pa_operation_unref(o); 61 | } 62 | 63 | // change the volume. newPct is a percentage 64 | void PAManager::changeVol(int newPct) { 65 | int newVol = PA_VOLUME_NORM * ((double)newPct / 100); 66 | pa_cvolume_set(&(sinkList[defaultSinkIndex].volume), sinkList[defaultSinkIndex].volume.channels, 67 | newVol); 68 | pa_operation* o = pa_context_set_sink_volume_by_index(context, sinkList[defaultSinkIndex].index, 69 | &(sinkList[defaultSinkIndex].volume), nullptr, nullptr); 70 | pa_operation_unref(o); 71 | } 72 | 73 | // return volume of the default sink as a percentage (0 - 100) 74 | int PAManager::getVolPct() { 75 | if (defaultSinkIndex == -1) 76 | return 0; 77 | 78 | // update sink volume 79 | struct cbStruct { 80 | std::vector& sink_list; 81 | int& def_index; 82 | pa_threaded_mainloop* mainloop; 83 | }; 84 | 85 | cbStruct s = {sinkList, defaultSinkIndex, mainloop}; 86 | 87 | auto callback = [](pa_context*, const pa_sink_info* sink, int eol, void* data) { 88 | auto items = static_cast(data); 89 | if (eol) { 90 | pa_threaded_mainloop_signal(items->mainloop, 0); 91 | return; 92 | } 93 | items->sink_list[items->def_index].volume = sink->volume; 94 | }; 95 | 96 | pa_operation* o = pa_context_get_sink_info_by_name( 97 | context, sinkList[defaultSinkIndex].name.c_str(), callback, static_cast(&s)); 98 | waitForOpFinish(o); 99 | 100 | return ceil(((double)sinkList[defaultSinkIndex].volume.values[0] / PA_VOLUME_NORM) * 100); 101 | } 102 | 103 | // get sink names into qml 104 | QStringList PAManager::getSinkList() { 105 | pa_threaded_mainloop_lock(mainloop); 106 | QStringList list; 107 | for (const auto& sink : sinkList) { 108 | list << sink.description.c_str(); 109 | } 110 | pa_threaded_mainloop_unlock(mainloop); 111 | return list; 112 | } 113 | 114 | void PAManager::changeSink(int sinkIndex) { 115 | pa_operation* op = pa_context_set_default_sink( 116 | context, sinkList[sinkIndex].name.c_str(), nullptr, nullptr); 117 | pa_operation_unref(op); 118 | defaultSinkIndex = sinkIndex; 119 | emit newDefaultSink(); 120 | } 121 | 122 | int PAManager::getDefaultSinkIndex() const { return defaultSinkIndex; } 123 | // get card names into qml 124 | QVariantList PAManager::getCardList() { 125 | // pa_threaded_mainloop_lock(mainloop); 126 | QVariantList list; 127 | for (const auto& card : cardList) 128 | list << QVariant::fromValue(card.get()); 129 | 130 | // pa_threaded_mainloop_unlock(mainloop); 131 | return list; 132 | } 133 | 134 | void PAManager::changeCardProfile(Card* card, const QString& profileName) { 135 | pa_threaded_mainloop_lock(mainloop); 136 | 137 | std::string oldSinkName = sinkList[defaultSinkIndex].name; 138 | 139 | std::string profile_name{}; 140 | for (int i = 0; i < card->availableProfiles.size(); i++) { 141 | const auto& profile = card->availableProfiles[i]; 142 | if (profile.description == profileName.toStdString()) { 143 | profile_name = profile.name; 144 | card->activeProfileIndex = i; 145 | break; 146 | } 147 | } 148 | 149 | auto callback = [](pa_context*, int success, void* data) { 150 | auto ml = static_cast(data); 151 | if (!success) 152 | std::cout << "Failed to switch to profile." << std::endl; 153 | pa_threaded_mainloop_signal(ml, 0); 154 | }; 155 | 156 | pa_operation* o = pa_context_set_card_profile_by_index( 157 | context, card->index, profile_name.c_str(), callback, mainloop); 158 | pa_threaded_mainloop_unlock(mainloop); 159 | 160 | // give time for card profile to be set 161 | waitForOpFinish(o); 162 | 163 | // rebuild sink list 164 | buildSinkList(); 165 | 166 | // PipeWire likes to change the sink after changing a card - switch back to 167 | // the current default sink, if it's still available. Otherwise, just guess 168 | // which sink the user wants 169 | pa_threaded_mainloop_lock(mainloop); 170 | 171 | bool oldSinkExists = true; 172 | struct cbStruct { 173 | bool& b; 174 | pa_threaded_mainloop* mainloop; 175 | std::string sink_name; 176 | } s{oldSinkExists, mainloop, oldSinkName}; 177 | 178 | auto cbf = [](pa_context* c, int suc, void* data) { 179 | auto items = static_cast(data); 180 | if (!suc) { 181 | items->b = false; 182 | std::cout << "Failed to set sink to " << items->sink_name << std::endl; 183 | } else 184 | std::cout << "Successfully changed to sink " << items->sink_name << std::endl; 185 | 186 | pa_threaded_mainloop_signal(items->mainloop, 0); 187 | }; 188 | o = pa_context_set_default_sink(context, oldSinkName.c_str(), cbf, static_cast(&s)); 189 | pa_threaded_mainloop_unlock(mainloop); 190 | waitForOpFinish(o); 191 | 192 | // guess the name of the newly created sink and try setting it 193 | if (!oldSinkExists) { 194 | pa_threaded_mainloop_lock(mainloop); 195 | // replace alsa_card with alsa_output 196 | std::string card_name = card->name; // alsa_card.XXX... 197 | card_name.replace(5, 4, "output"); // alsa_output 198 | 199 | // remove "output:" from profile name 200 | profile_name.replace(0, 7, ""); 201 | 202 | // some profile names seem to have two colons? the stuff after the last 203 | // one seems to be the sink name 204 | auto colonLoc = profile_name.find(':'); 205 | if (colonLoc != std::string::npos) 206 | profile_name.replace(0, colonLoc + 1, ""); 207 | 208 | std::string guessedSink = card_name + "." + profile_name; 209 | 210 | s.sink_name = guessedSink; 211 | o = pa_context_set_default_sink(context, guessedSink.c_str(), cbf, static_cast(&s)); 212 | pa_threaded_mainloop_unlock(mainloop); 213 | waitForOpFinish(o); 214 | } 215 | 216 | // small sleep to give time for default sink to be updated 217 | pa_msleep(100); 218 | getDefaultSink(); 219 | } 220 | 221 | // get card profiles into qml 222 | QStringList Card::getProfileList() const { 223 | QStringList list; 224 | for (const auto& profile : availableProfiles) 225 | list << profile.description.c_str(); 226 | return list; 227 | } 228 | 229 | Profile Card::getActiveProfile() { 230 | emit profilesChanged(); 231 | return availableProfiles[activeProfileIndex]; 232 | } 233 | 234 | /* helper functions */ 235 | 236 | void PAManager::getDefaultSink() { 237 | struct cbStruct { 238 | std::vector& sink_list; 239 | int& def_index; 240 | pa_threaded_mainloop* mainloop; 241 | }; 242 | cbStruct s = {sinkList, defaultSinkIndex, mainloop}; 243 | 244 | auto callback = [](pa_context*, const pa_server_info* info, void* data) { 245 | auto items = static_cast(data); 246 | std::string defaultSinkName = info->default_sink_name; 247 | for (int i = 0; i < items->sink_list.size(); i++) { 248 | if (items->sink_list[i].name == defaultSinkName) { 249 | items->def_index = i; 250 | break; 251 | } 252 | } 253 | pa_threaded_mainloop_signal(items->mainloop, 0); 254 | }; 255 | pa_operation* o = pa_context_get_server_info(context, callback, static_cast(&s)); 256 | waitForOpFinish(o); 257 | emit newDefaultSink(); 258 | } 259 | 260 | void PAManager::buildSinkList() { 261 | pa_threaded_mainloop_lock(mainloop); 262 | if (!sinkList.empty()) 263 | sinkList.clear(); 264 | 265 | struct cbStruct { 266 | std::vector& sink_list; 267 | pa_threaded_mainloop* mainloop; 268 | }; 269 | cbStruct s = {sinkList, mainloop}; 270 | 271 | auto callback = [](pa_context*, const pa_sink_info* sink, int eol, void* data) { 272 | auto items = static_cast(data); 273 | if (eol) { 274 | pa_threaded_mainloop_signal(items->mainloop, 0); 275 | return; 276 | } 277 | Sink s = {sink->name, sink->description, sink->index, sink->volume}; 278 | items->sink_list.push_back(s); 279 | }; 280 | 281 | pa_operation* o = pa_context_get_sink_info_list(context, callback, static_cast(&s)); 282 | pa_threaded_mainloop_unlock(mainloop); 283 | waitForOpFinish(o); 284 | emit sinksChanged(); 285 | } 286 | 287 | void PAManager::buildCardList() { 288 | struct cbStruct { 289 | std::vector>& card_list; 290 | pa_threaded_mainloop* mainloop; 291 | }; 292 | cbStruct s = {cardList, mainloop}; 293 | 294 | auto callback = [](pa_context*, const pa_card_info* card, int eol, void* data) { 295 | auto items = static_cast(data); 296 | if (eol) { 297 | if (pa_threaded_mainloop_in_thread(items->mainloop)) 298 | pa_threaded_mainloop_signal(items->mainloop, 0); 299 | return; 300 | } 301 | 302 | auto c = std::make_unique(); 303 | c->name = card->name; 304 | c->index = card->index; 305 | const char* desc = pa_proplist_gets(card->proplist, "device.description"); 306 | c->description = (desc) ? desc : card->name; 307 | 308 | for (unsigned int i = 0; i < card->n_profiles; i++) { 309 | if (card->profiles2[i]->available) { 310 | Profile p; 311 | p.name = card->profiles2[i]->name; 312 | p.description = card->profiles2[i]->description; 313 | p.active = (card->profiles2[i] == card->active_profile2); 314 | c->availableProfiles.push_back(p); 315 | if (p.active) 316 | c->activeProfileIndex = i; 317 | } 318 | } 319 | items->card_list.push_back(std::move(c)); 320 | }; 321 | 322 | pa_operation* o = pa_context_get_card_info_list(context, callback, static_cast(&s)); 323 | waitForOpFinish(o); 324 | } 325 | 326 | bool PAManager::saveConfig() { 327 | // class to make sure mainloop is always unlocked at the end 328 | class threadChk { 329 | private: 330 | pa_threaded_mainloop* ml; 331 | 332 | public: 333 | explicit threadChk(pa_threaded_mainloop* ml_) : ml(ml_) { pa_threaded_mainloop_lock(ml); } 334 | ~threadChk() { pa_threaded_mainloop_unlock(ml); } 335 | }; 336 | threadChk T(mainloop); 337 | 338 | std::string sink_name = sinkList[defaultSinkIndex].name; 339 | 340 | // card name should be first part of sink name (alsa_card.pci_XXXX) 341 | std::string card_name = sink_name; 342 | 343 | // change "output" to "card" 344 | card_name.replace(5, 6, "card"); 345 | auto profile_name_loc = card_name.rfind('.'); 346 | 347 | // save profile name from end of sink name 348 | std::string profile_name = card_name.substr(profile_name_loc + 1); 349 | profile_name = "output:" + profile_name; 350 | card_name.replace(card_name.begin() + profile_name_loc, card_name.end(), ""); 351 | 352 | QDir config_dir(strings::config_dir_loc); 353 | if (!config_dir.exists()) { 354 | if (!config_dir.mkpath(strings::config_dir_loc)) { 355 | std::cerr << "Could not create config directory!" << std::endl; 356 | return false; 357 | } 358 | } 359 | QFile config(strings::audioconfig_loc); 360 | if (!config.open(QFile::WriteOnly)) { 361 | std::cerr << "Could not open audioconfig.txt for writing!" << std::endl; 362 | return false; 363 | } 364 | QTextStream out(&config); 365 | out << "Sink: " << sink_name.c_str() << '\n'; 366 | out << "Card: " << card_name.c_str() << '\n'; 367 | out << "Profile: " << profile_name.c_str() << '\n'; 368 | 369 | return true; 370 | } 371 | 372 | void PAManager::loadConfig() { 373 | pa_threaded_mainloop_lock(mainloop); 374 | QFile config(strings::audioconfig_loc); 375 | if (!config.open(QFile::ReadOnly)) { 376 | std::cout << "No configuration file found." << std::endl; 377 | pa_threaded_mainloop_unlock(mainloop); 378 | return; 379 | } 380 | 381 | QTextStream in(&config); 382 | 383 | QString sink = in.readLine(); 384 | // get rid of "Sink: " 385 | sink.replace(0, 6, ""); 386 | 387 | QString card = in.readLine(); 388 | // get rid of "Card: " 389 | card.replace(0, 6, ""); 390 | 391 | QString profile = in.readLine(); 392 | // get rid of "Profile: " 393 | profile.replace(0, 9, ""); 394 | 395 | config.close(); 396 | 397 | // attempt to set profile 398 | struct cbStruct { 399 | QString& cardStr; 400 | QString& profileStr; 401 | pa_threaded_mainloop* mainloop; 402 | } s{card, profile, mainloop}; 403 | 404 | auto callback = [](pa_context* c, int success, void* data) { 405 | auto items = static_cast(data); 406 | if (!success) 407 | std::clog << "Could not set profile " << items->profileStr.toStdString() << " on card " 408 | << items->cardStr.toStdString() << std::endl; 409 | pa_threaded_mainloop_signal(items->mainloop, 0); 410 | }; 411 | 412 | pa_operation* o = pa_context_set_card_profile_by_name( 413 | context, card.toUtf8(), profile.toUtf8(), callback, &s); 414 | pa_threaded_mainloop_unlock(mainloop); 415 | waitForOpFinish(o); 416 | 417 | pa_threaded_mainloop_lock(mainloop); 418 | struct cbStruct2 { 419 | QString& sinkStr; 420 | pa_threaded_mainloop* mainloop; 421 | } s2{sink, mainloop}; 422 | 423 | auto callback2 = [](pa_context* c, int success, void* data) { 424 | auto items = static_cast(data); 425 | if (!success) 426 | std::clog << "Could not set sink to " << items->sinkStr.toStdString() << std::endl; 427 | else 428 | std::clog << "Set sink to " << items->sinkStr.toStdString() << std::endl; 429 | pa_threaded_mainloop_signal(items->mainloop, 0); 430 | }; 431 | o = pa_context_set_default_sink(context, sink.toUtf8(), callback2, &s2); 432 | pa_threaded_mainloop_unlock(mainloop); 433 | waitForOpFinish(o); 434 | // small sleep to give time for sink to be updated (seems to be a PipeWire issue?) 435 | pa_msleep(20); 436 | } 437 | 438 | bool PAManager::Sink::operator==(const Sink& other) const { 439 | return (name == other.name && description == other.description && index == other.index); 440 | } 441 | -------------------------------------------------------------------------------- /src/vrmanager.cpp: -------------------------------------------------------------------------------- 1 | #include "vrmanager.h" 2 | 3 | #include "openvr.h" 4 | #include "strs.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | using namespace vr; 24 | 25 | VRManager::VRManager(QQuickView* w, QQuickRenderControl* rc) 26 | : window(w), renderCtrl(rc), overlayWidth(2100), overlayHeight(1200) { 27 | 28 | // expose overlay size to QML 29 | window->engine()->rootContext()->setContextProperty("screenWidth", overlayWidth); 30 | window->engine()->rootContext()->setContextProperty("screenHeight", overlayHeight); 31 | 32 | // signal to create scene graph 33 | connect(window, SIGNAL(sceneGraphInitialized()), this, SLOT(prepareSceneGraph())); 34 | 35 | window->setVulkanInstance(&instance); 36 | 37 | initVR(); 38 | initVulkan(); 39 | getPhysicalDevice(); 40 | createLogicalDevice(); 41 | } 42 | 43 | void VRManager::prepareSceneGraph() { 44 | createImage(); 45 | createCommandPool(); 46 | window->setRenderTarget(QQuickRenderTarget::fromVulkanImage( 47 | image, curLayout, QSize(overlayWidth, overlayHeight))); 48 | 49 | // create timer 50 | checkTimer = std::make_unique(this); 51 | connect(checkTimer.get(), SIGNAL(timeout()), this, SLOT(checkRender())); 52 | checkTimer->setInterval(20); 53 | checkTimer->start(); 54 | } 55 | 56 | void VRManager::pollEvents() { 57 | VREvent_t event{}; 58 | // quitting 59 | while (VRSystem()->PollNextEvent(&event, sizeof(event))) { 60 | switch (event.eventType) { 61 | case VREvent_Quit: { 62 | VRSystem()->AcknowledgeQuit_Exiting(); 63 | std::cout << "Exiting!" << std::endl; 64 | QGuiApplication::quit(); 65 | } break; 66 | default: 67 | break; 68 | } 69 | } 70 | 71 | // input 72 | while (VROverlay()->PollNextOverlayEvent(overlay, &event, sizeof(event))) { 73 | switch (event.eventType) { 74 | 75 | case (VREvent_MouseMove): { 76 | // SteamVR (0,0) is bottom left, while Qt (0,0) is top left - invert y 77 | QPointF mousePos(event.data.mouse.x, overlayHeight - event.data.mouse.y); 78 | QMouseEvent mouseEvent(QEvent::MouseMove, mousePos, window->mapToGlobal(mousePos), 79 | Qt::NoButton, activeMouseButtons, Qt::NoModifier); 80 | 81 | QGuiApplication::sendEvent(window, &mouseEvent); 82 | lastPos = mousePos; 83 | } break; 84 | 85 | case (VREvent_MouseButtonDown): { 86 | QPointF mousePos(event.data.mouse.x, overlayHeight - event.data.mouse.y); 87 | activeMouseButtons |= Qt::LeftButton; 88 | 89 | QMouseEvent mouseEvent(QEvent::MouseButtonPress, mousePos, 90 | window->mapToGlobal(mousePos), Qt::LeftButton, activeMouseButtons, 91 | Qt::NoModifier); 92 | 93 | QGuiApplication::sendEvent(window, &mouseEvent); 94 | lastPos = mousePos; 95 | } break; 96 | 97 | case (VREvent_MouseButtonUp): { 98 | QPointF mousePos(event.data.mouse.x, overlayHeight - event.data.mouse.y); 99 | activeMouseButtons &= ~Qt::LeftButton; 100 | 101 | QMouseEvent mouseEvent(QEvent::MouseButtonRelease, mousePos, 102 | window->mapToGlobal(mousePos), Qt::LeftButton, activeMouseButtons, 103 | Qt::NoModifier); 104 | 105 | QGuiApplication::sendEvent(window, &mouseEvent); 106 | lastPos = mousePos; 107 | } break; 108 | 109 | case (VREvent_ScrollDiscrete): { 110 | QPoint scrollData(0, -event.data.scroll.ydelta); 111 | QWheelEvent wheelEvent(lastPos, window->mapToGlobal(lastPos), QPoint(), scrollData, 112 | activeMouseButtons, Qt::NoModifier, Qt::NoScrollPhase, false, 113 | Qt::MouseEventNotSynthesized); 114 | QGuiApplication::sendEvent(window, &wheelEvent); 115 | 116 | } break; 117 | } 118 | } 119 | } 120 | 121 | void VRManager::checkRender() { 122 | // check if overlay is setup 123 | if (!VROverlay() || overlay == k_ulOverlayHandleInvalid) { 124 | VROverlay()->SetOverlayTexture(overlay, nullptr); 125 | return; 126 | } 127 | 128 | render(); 129 | pollEvents(); 130 | } 131 | 132 | void VRManager::render() { 133 | renderCtrl->polishItems(); 134 | renderCtrl->beginFrame(); 135 | renderCtrl->sync(); 136 | renderCtrl->render(); 137 | renderCtrl->endFrame(); 138 | curLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; // QQuickRenderControl transitions image 139 | 140 | transitionImageLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); 141 | 142 | VRVulkanTextureData_t texData{}; 143 | texData.m_nImage = (uint64_t)image; 144 | texData.m_pDevice = device; 145 | texData.m_pPhysicalDevice = physicalDevice; 146 | texData.m_pInstance = instance.vkInstance(); 147 | texData.m_pQueue = graphicsQueue; 148 | texData.m_nQueueFamilyIndex = graphicsFamily; 149 | texData.m_nWidth = overlayWidth; 150 | texData.m_nHeight = overlayHeight; 151 | texData.m_nFormat = VK_FORMAT_R8G8B8A8_UNORM; 152 | texData.m_nSampleCount = 1; 153 | 154 | Texture_t tex{}; 155 | tex.handle = static_cast(&texData); 156 | tex.eType = TextureType_Vulkan; 157 | tex.eColorSpace = ColorSpace_Auto; 158 | 159 | VROverlay()->SetOverlayTexture(overlay, &tex); 160 | } 161 | void VRManager::buildOverlay() { 162 | VROverlay()->CreateDashboardOverlay( 163 | strings::app_key, strings::overlay_friendly_name, &overlay, &icon); 164 | VROverlay()->SetOverlayWidthInMeters(overlay, 2.7f); 165 | VROverlay()->SetOverlayInputMethod(overlay, VROverlayInputMethod_Mouse); 166 | 167 | // SetOverlayMouseScale basically sets bounds of the overlay 168 | HmdVector2_t mouseScale = {(float)overlayWidth, (float)overlayHeight}; 169 | VROverlay()->SetOverlayMouseScale(overlay, &mouseScale); 170 | 171 | // enable scroll events 172 | VROverlay()->SetOverlayFlag(overlay, VROverlayFlags_SendVRDiscreteScrollEvents, true); 173 | 174 | // set up icon 175 | if (temp_dir.isValid()) { 176 | const QString temp_file = temp_dir.path() + "/speaker-256.png"; 177 | if (QFile::copy(":/speaker-256.png", temp_file)) { 178 | VROverlay()->SetOverlayFromFile(icon, temp_file.toUtf8()); 179 | } 180 | } 181 | } 182 | 183 | void VRManager::uninstall() { initVR(true); } 184 | void VRManager::initVR(bool uninstall) { 185 | EVRInitError peError = EVRInitError::VRInitError_None; 186 | VR_Init(&peError, EVRApplicationType::VRApplication_Overlay); 187 | if (peError != EVRInitError::VRInitError_None) 188 | throw std::runtime_error(std::string("Could not initialize VR: ") 189 | + VR_GetVRInitErrorAsEnglishDescription(peError)); 190 | 191 | // Manifests seem to need to always exist: install to ~/.config/vrdio 192 | QDir config_loc(strings::config_dir_loc); 193 | QFileInfo manifest(strings::vrmanifest_loc); 194 | 195 | if (uninstall) { 196 | if (VRApplications()->IsApplicationInstalled(strings::app_key)) { 197 | auto err = VRApplications()->RemoveApplicationManifest( 198 | manifest.absoluteFilePath().toUtf8()); 199 | if (err != EVRApplicationError::VRApplicationError_None) { 200 | throw std::runtime_error(std::string("Failed to remove manifest: ") 201 | + VRApplications()->GetApplicationsErrorNameFromEnum(err)); 202 | } 203 | QFile::remove(manifest.absoluteFilePath()); 204 | 205 | std::cout << "Manifest uninstalled." << std::endl; 206 | } else 207 | std::cout << "No manifest to uninstall." << std::endl; 208 | } else { 209 | bool success = true; 210 | if (!manifest.exists()) { // create ~/.config/vrdio 211 | if (!config_loc.exists()) 212 | success = config_loc.mkpath(strings::config_dir_loc); 213 | 214 | if (success) { 215 | // build manifest 216 | QJsonObject top_level; 217 | top_level.insert("source", "supreme"); 218 | QJsonObject obj, strings, en_us; 219 | obj.insert("app_key", strings::app_key); 220 | obj.insert("launch_type", "binary"); 221 | obj.insert("binary_path_linux", QGuiApplication::applicationFilePath()); 222 | obj.insert("is_dashboard_overlay", true); 223 | en_us.insert("name", "VRdio"); 224 | en_us.insert("description", "Audio control from VR for Linux"); 225 | strings.insert("en_us", en_us); 226 | obj.insert("strings", strings); 227 | top_level.insert("applications", QJsonArray({obj})); 228 | 229 | QJsonDocument doc; 230 | doc.setObject(top_level); 231 | 232 | // save file 233 | QFile file(manifest.absoluteFilePath()); 234 | file.open(QFile::WriteOnly); 235 | file.write(doc.toJson()); 236 | } else { 237 | std::clog << "Couldn't create config path to install manifest!" << std::endl; 238 | } 239 | } 240 | if (success && !VRApplications()->IsApplicationInstalled(strings::app_key)) { 241 | // add manifest 242 | std::clog << "Installing manifest..." << std::endl; 243 | auto err = 244 | VRApplications()->AddApplicationManifest(manifest.absoluteFilePath().toUtf8()); 245 | if (err != EVRApplicationError::VRApplicationError_None) { 246 | std::clog << "Failed to add manifest: " 247 | << VRApplications()->GetApplicationsErrorNameFromEnum(err) << std::endl; 248 | } else 249 | std::clog << "Manifest installed." << std::endl; 250 | 251 | // set up auto launch 252 | err = VRApplications()->SetApplicationAutoLaunch(strings::app_key, true); 253 | if (err != EVRApplicationError::VRApplicationError_None) { 254 | std::clog << "Failed to enable autostart: " 255 | << VRApplications()->GetApplicationsErrorNameFromEnum(err) << std::endl; 256 | } else 257 | std::clog << "Autostart enabled." << std::endl; 258 | } 259 | } 260 | } 261 | 262 | void VRManager::initVulkan() { 263 | // get extensions required by SteamVR 264 | uint32_t extSize = VRCompositor()->GetVulkanInstanceExtensionsRequired(nullptr, 0); 265 | QByteArray extList; 266 | extList.resize(extSize); 267 | VRCompositor()->GetVulkanInstanceExtensionsRequired(extList.data(), extSize); 268 | QByteArrayList reqExts = extList.split(' '); 269 | 270 | instance.setExtensions(reqExts); 271 | 272 | if (!instance.create()) { 273 | throw std::runtime_error("Failed to create Vulkan instance"); 274 | } 275 | vkFuncs = instance.functions(); 276 | } 277 | 278 | void VRManager::getPhysicalDevice() { 279 | uint32_t devCount = 0; 280 | vkFuncs->vkEnumeratePhysicalDevices(instance.vkInstance(), &devCount, nullptr); 281 | VkPhysicalDevice physicalDevices[devCount]; 282 | vkFuncs->vkEnumeratePhysicalDevices(instance.vkInstance(), &devCount, physicalDevices); 283 | physicalDevice = physicalDevices[0]; 284 | } 285 | 286 | void VRManager::createLogicalDevice() { 287 | // get graphics queue family 288 | uint32_t queueFamilyCount = 0; 289 | vkFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr); 290 | VkQueueFamilyProperties queueFamilies[queueFamilyCount]; 291 | vkFuncs->vkGetPhysicalDeviceQueueFamilyProperties( 292 | physicalDevice, &queueFamilyCount, queueFamilies); 293 | 294 | int i = 0; 295 | for (const auto& queueFamily : queueFamilies) { 296 | if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { 297 | graphicsFamily = i; 298 | break; 299 | } 300 | i++; 301 | } 302 | 303 | // specify queue creation info 304 | VkDeviceQueueCreateInfo queueCreateInfo{}; 305 | queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; 306 | queueCreateInfo.queueFamilyIndex = graphicsFamily; 307 | queueCreateInfo.queueCount = 1; 308 | float queuePriority = 1.0f; 309 | queueCreateInfo.pQueuePriorities = &queuePriority; 310 | 311 | // specify device features 312 | VkPhysicalDeviceFeatures deviceFeatures{}; 313 | 314 | // device create info 315 | VkDeviceCreateInfo createInfo{}; 316 | createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; 317 | createInfo.pQueueCreateInfos = &queueCreateInfo; 318 | createInfo.queueCreateInfoCount = 1; 319 | createInfo.pEnabledFeatures = &deviceFeatures; 320 | 321 | // get required device extensions 322 | uint32_t exts2 = 0; 323 | vkFuncs->vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &exts2, nullptr); 324 | std::vector ext2list(exts2); 325 | vkFuncs->vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &exts2, ext2list.data()); 326 | 327 | uint32_t extSize = 328 | VRCompositor()->GetVulkanDeviceExtensionsRequired(physicalDevice, nullptr, 0); 329 | QByteArray extList; 330 | extList.resize(extSize); 331 | VRCompositor()->GetVulkanDeviceExtensionsRequired(physicalDevice, extList.data(), extSize); 332 | QByteArrayList reqExts = extList.split(' '); 333 | std::vector reqExts_vk; 334 | for (auto& str : reqExts) { 335 | str.push_back('\0'); // add null terminators 336 | reqExts_vk.push_back(str.data()); 337 | } 338 | 339 | createInfo.enabledExtensionCount = reqExts.size(); 340 | createInfo.ppEnabledExtensionNames = reqExts_vk.data(); 341 | createInfo.enabledLayerCount = 0; 342 | vkFuncs->vkCreateDevice(physicalDevice, &createInfo, nullptr, &device); 343 | 344 | devFuncs = instance.deviceFunctions(device); 345 | // save graphics queue 346 | devFuncs->vkGetDeviceQueue(device, graphicsFamily, 0, &graphicsQueue); 347 | 348 | // set qgraphicsdevice 349 | window->setGraphicsDevice( 350 | QQuickGraphicsDevice::fromDeviceObjects(physicalDevice, device, graphicsFamily)); 351 | } 352 | 353 | void VRManager::createImage() { 354 | // create vulkan image for rendering 355 | VkImageCreateInfo imageInfo{}; 356 | imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; 357 | imageInfo.imageType = VK_IMAGE_TYPE_2D; 358 | imageInfo.extent.width = overlayWidth; 359 | imageInfo.extent.height = overlayHeight; 360 | imageInfo.extent.depth = 1; 361 | imageInfo.mipLevels = 1; 362 | imageInfo.arrayLayers = 1; 363 | imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; 364 | imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; 365 | imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; 366 | curLayout = VK_IMAGE_LAYOUT_UNDEFINED; 367 | imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; 368 | imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; 369 | imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; 370 | devFuncs->vkCreateImage(device, &imageInfo, nullptr, &image); 371 | 372 | // allocate memory for image 373 | VkMemoryRequirements memReqs; 374 | devFuncs->vkGetImageMemoryRequirements(device, image, &memReqs); 375 | 376 | VkMemoryAllocateInfo allocInfo{}; 377 | allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 378 | allocInfo.allocationSize = memReqs.size; 379 | allocInfo.memoryTypeIndex = UINT32_MAX; 380 | 381 | VkPhysicalDeviceMemoryProperties memProps; 382 | vkFuncs->vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps); 383 | for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) { 384 | if (memProps.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { 385 | allocInfo.memoryTypeIndex = i; 386 | break; 387 | } 388 | } 389 | 390 | if (allocInfo.memoryTypeIndex == UINT32_MAX) 391 | exit(1); 392 | 393 | devFuncs->vkAllocateMemory(device, &allocInfo, nullptr, &imageMem); 394 | 395 | devFuncs->vkBindImageMemory(device, image, imageMem, 0); 396 | } 397 | 398 | void VRManager::transitionImageLayout(VkImageLayout newLayout) { 399 | if (newLayout == curLayout) 400 | return; // no transition needed 401 | 402 | VkCommandBuffer cmdBuffer = beginSingleTimeCommands(); 403 | VkImageMemoryBarrier barrier{}; 404 | barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; 405 | barrier.oldLayout = curLayout; 406 | barrier.newLayout = newLayout; 407 | barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; 408 | barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; 409 | barrier.image = image; 410 | barrier.subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 411 | .baseMipLevel = 0, 412 | .levelCount = 1, 413 | .baseArrayLayer = 0, 414 | .layerCount = 1}; 415 | 416 | VkPipelineStageFlags srcFlags, dstFlags; 417 | if (curLayout == VK_IMAGE_LAYOUT_UNDEFINED 418 | && newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { 419 | barrier.srcAccessMask = 0; 420 | barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; 421 | 422 | srcFlags = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; 423 | dstFlags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; 424 | } else if (curLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 425 | && newLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { 426 | barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; 427 | barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; 428 | 429 | srcFlags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; 430 | dstFlags = VK_PIPELINE_STAGE_TRANSFER_BIT; 431 | } else if (curLayout == VK_IMAGE_LAYOUT_UNDEFINED 432 | && newLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { 433 | barrier.srcAccessMask = 0; 434 | barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; 435 | 436 | srcFlags = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; 437 | dstFlags = VK_PIPELINE_STAGE_TRANSFER_BIT; 438 | } else { 439 | throw std::runtime_error("Invalid layout transition!"); 440 | } 441 | 442 | devFuncs->vkCmdPipelineBarrier( 443 | cmdBuffer, srcFlags, dstFlags, 0, 0, nullptr, 0, nullptr, 1, &barrier); 444 | 445 | endSingleTimeCommands(cmdBuffer); 446 | curLayout = newLayout; 447 | } 448 | 449 | void VRManager::createCommandPool() { 450 | VkCommandPoolCreateInfo poolInfo{}; 451 | poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; 452 | poolInfo.queueFamilyIndex = graphicsFamily; 453 | 454 | devFuncs->vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool); 455 | } 456 | 457 | VkCommandBuffer VRManager::beginSingleTimeCommands() { 458 | VkCommandBufferAllocateInfo allocInfo{}; 459 | allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; 460 | allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; 461 | allocInfo.commandPool = commandPool; 462 | allocInfo.commandBufferCount = 1; 463 | 464 | VkCommandBuffer commandBuffer; 465 | devFuncs->vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); 466 | 467 | VkCommandBufferBeginInfo beginInfo{}; 468 | beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; 469 | beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; 470 | 471 | devFuncs->vkBeginCommandBuffer(commandBuffer, &beginInfo); 472 | 473 | return commandBuffer; 474 | } 475 | 476 | void VRManager::endSingleTimeCommands(VkCommandBuffer commandBuffer) { 477 | devFuncs->vkEndCommandBuffer(commandBuffer); 478 | 479 | VkSubmitInfo submitInfo{}; 480 | submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; 481 | submitInfo.commandBufferCount = 1; 482 | submitInfo.pCommandBuffers = &commandBuffer; 483 | 484 | devFuncs->vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); 485 | devFuncs->vkQueueWaitIdle(graphicsQueue); 486 | 487 | devFuncs->vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); 488 | } 489 | 490 | VRManager::~VRManager() { 491 | devFuncs->vkDestroyCommandPool(device, commandPool, nullptr); 492 | devFuncs->vkDestroyImage(device, image, nullptr); 493 | devFuncs->vkFreeMemory(device, imageMem, nullptr); 494 | VR_Shutdown(); 495 | } 496 | --------------------------------------------------------------------------------