ValveSoftware / openvr

OpenVR SDK
http://steamvr.com
BSD 3-Clause "New" or "Revised" License
6.1k stars 1.28k forks source link

Tell vrmonitor.exe / StreamVR to exit gracefully? #779

Closed zezba9000 closed 6 years ago

zezba9000 commented 6 years ago

Hi we have an app that manages some SteamVR features for our systems. Currently we're force quitting vrmonitor.exe and its child processes to manager computer/system states. Sending window close events doesn't work sadly in the vrmonitor.

I would like to do this properly with the OpenVR API but don't see any method to achieve this. Is this possible or am I missing something?

TheDeveloperGuy commented 6 years ago

https://github.com/ValveSoftware/openvr/issues/473

zezba9000 commented 6 years ago

@TheDeveloperGuy That wont work. This isn't for a OpenVR "driver". This is for an OpenVR utility which uses the other header file that doesn't include that method.

MHDante commented 6 years ago

Do you want to see a neat trick? start openvr and click here:

http://127.0.0.1:8998/console_command.action?sCommand=quit

zezba9000 commented 6 years ago

@MHDante Interesting. However this isn't working on my PC.

I get this result:

{
   "jsonid" : "vr_console_command",
   "sError" : "Connection Broken"
}
TheDeveloperGuy commented 6 years ago

Make a 'hook' driver and hook the DebugRequest() method for any HMD that gets Activate() called successfully and send the driver shutdown event from your DebugRequest() override.

zezba9000 commented 6 years ago

@MHDante Your info did come in handy but it was missing some stuff. You need to first create a WebSocket. The method below can be ported to C# or any lang that has WebSocket clients. (method below works)

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>

    <script type="text/javascript" src="http://localhost:8998/shared/js/jquery-3.1.1.min.js"></script>
</head>
<body>
    <script>
        function OnWebSocketOpen() {
            console.log("Open");
            m_wsWebSocketToServer.send( "console_open" );

            $.ajax({
                type: "GET",
                url: 'http://localhost:8998/console_command.action?sCommand=quit',
                data:{},
                async:true,
                dataType : 'jsonp',   //you may use jsonp for cross origin request
                crossDomain:true,
                success: function(data, status, xhr) {
                    alert(xhr.getResponseHeader('Location'));
                },
                complete: function( jqXHR, sTextStatus ) {
                    console.log("Close");
                    m_wsWebSocketToServer.send( "console_close" );
                }
            });
        }

        function OnWebSocketMessage() {

        }

        // connect
        console.log("Starting");
        var m_wsWebSocketToServer = new WebSocket( "ws://localhost:8998" );
        m_wsWebSocketToServer.addEventListener( "open", OnWebSocketOpen );
        m_wsWebSocketToServer.addEventListener( "message", OnWebSocketMessage );
        console.log("End");
    </script>
</body>
</html>
vinayak-vc commented 5 years ago

Hi @zezba9000

How do I use this code

I mean for now I have stored this code in HTML file and starting it by Process.

is there any other convenient way? I am using Windows Forms (C#) Thanks in Advance.

zezba9000 commented 5 years ago

@vinayak-vc To test out that code copy and put it in a .html file.

BUT they have added in the ability to send a normal close window event to the vrmonitor window. Use that instead.

Code below is a C# example of how to determine what the main window is:

private static BOOL IsMainWindow(HWND handle)
        {
            bool valid = GetWindow(handle, GW_OWNER) == HANDLE.Zero && IsWindowVisible(handle) != FALSE;
            return valid ? TRUE : FALSE;
        }

Just look up how to close the main Window from C++ for more info. NOTE: Getting the process in C# and calling CloseMainWindow probably wont work as your C# process probably didn't open vrmonitor directly.

vinayak-vc commented 5 years ago

@zezba9000

Can you tell me how do I run that HTML file without browser I mean is that possible to run an HTML code in the command window and kill it after some time?

Here in my case, I am developing one lobby where you can start a game in VR so when the game is closed directly with the Oculus Menu button then the steamVR is losing focus so even if I start my lobby again the lobby will start but the Oculus won't show the game window in it. So I am checking if the game is closed directly then steamvr will be killed and start again.

So when I kill the steamvr using that HTML file it works like charm but also causing distraction as it opens in a browser so I need an alternative to that

Thanks for listing to me.

zezba9000 commented 5 years ago

Yes you can use C# with that HTTP method.

For .NET Framework use: https://github.com/sta/websocket-sharp Then you can use this method below (Just replace 'HTTPExtensions.MakeRequestAsync' with your standard C# HttpWebRequest logic)

        public static bool SendCommand(string command)
        {
            try
            {
                using (var ws = new WebSocket("ws://localhost:8998"))
                {
                    bool openWait = true;
                    ws.OnOpen += async delegate (object sender, EventArgs e)
                    {
                        ws.Send("console_open");
                        await HTTPExtensions.MakeRequestAsync("http://localhost:8998/console_command.action?sCommand=" + command, "GET");
                    };

                    ws.Connect();
                    int waitCount = 3;
                    while (openWait && waitCount != 0)
                    {
                        Thread.Sleep(1000);
                        --waitCount;
                    }

                    if (ws.IsAlive) ws.Send("console_close");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Failed to send VRMonitor close event: " + e.Message);
                return false;
            }

            return true;
        }

Then invoke the method like so: "SendCommand("quit");"

JoeLudwig commented 5 years ago

Fair warning: it's entirely possible that the HTTP protocol between the web console and vrserver will change down the line and break anything that tries to use it that isn't the web console. At the very least, we should probably be using POST instead of GET.

zezba9000 commented 5 years ago

@JoeLudwig Yep, thats why I suggested just sending a normal Windows close event as this is the correct approach from an external process. This WebSocket hack was originally done when VRMonitor didn't respect standard windows close events. Now it does thank god ;)

vinayak-vc commented 5 years ago

Can you please post some code snippets which I can follow for "find window" and close them and also find relative windows to a similar process. I mean what I have to do?

Thanks.

zezba9000 commented 5 years ago

@vinayak-vc https://stackoverflow.com/questions/2531828/how-to-enumerate-all-windows-belonging-to-a-particular-process-using-net

Look at the answer from "Konstantin Spirin" in that StackOverflow link.

The reason you need to close all windows is because if there are sub windows (aka non-main windows) telling just the main window to close may not actually close the application.

vinayak-vc commented 5 years ago

@zezba9000
The solution worked like charm. Thanks for your support. I have managed to complete the task that I wanted to do. Thanks Again.

here is the final code

class CloseSTeamVRcs
    {
        public static void CloseSteam()
        {
            try
            {
                foreach (var handle in EnumerateProcessWindowHandles(
                       Process.GetProcessesByName("vrmonitor").First().Id))
                {
                    StringBuilder message = new StringBuilder(1000);
                    SendMessage(handle, WM_CLOSE, message.Capacity, message);
                    Debug.WriteLine(message);
                }
            }
            catch (System.Exception) { }
        }
        private const uint WM_CLOSE = 0x10;

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, int wParam,
            StringBuilder lParam);
        delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll")]
        static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn,
            IntPtr lParam);

        static IEnumerable<IntPtr> EnumerateProcessWindowHandles(int processId)
        {
            try
            {
                var handles = new List<IntPtr>();

                foreach (ProcessThread thread in Process.GetProcessById(processId).Threads)
                    EnumThreadWindows(thread.Id,
                        (hWnd, lParam) => { handles.Add(hWnd); return true; }, IntPtr.Zero);

                return handles;
            }
            catch (System.Exception) { return null; }
        }
    }
vinayak-vc commented 5 years ago

Hiii @zezba9000

I have another question regarding SteamVR

Do you know how to make inractive overlay in SteamVR? I mean several images and buttons on the overlay.

I want to show one windows form as an overlay in steamvr.

Thanks

zezba9000 commented 5 years ago

Here is the C lib code.

#include "stdafx.h"
#include "openvr.h"
#include <stdio.h>
#include <string>
#define EXPORT __declspec(dllexport)

vr::VROverlayHandle_t mainHandle = 0;
std::string lastError;

extern "C"
{
    EXPORT int VR_OpenVR_GetLastError(char* errorBuffer, int errorBufferLength)
    {
        return lastError.copy(errorBuffer, errorBufferLength);
    }

    EXPORT bool VR_OpenVR_Init(char* overlayKey, char* overlayFriendlyName, float overlayWidthInMeters)
    {
        auto systemError = vr::VRInitError_None;
        vr::IVRSystem *system = vr::VR_Init(&systemError, vr::VRApplication_Overlay);
        if (systemError != vr::VRInitError_None)
        {
            lastError = "VR_Init failed: " + std::to_string(systemError);
            return false;
        }

        auto compositor = vr::VRCompositor();
        if (compositor == nullptr)
        {
            lastError = "Getting VRCompositor failed"; 
            return false;
        }

        auto overlay = vr::VROverlay();
        if (overlay == nullptr)
        {
            lastError = "Getting VROverlay failed"; 
            return false;
        }

        vr::VROverlayError overlayError = vr::VROverlay()->CreateOverlay(overlayKey, overlayFriendlyName, &mainHandle);
        if (overlayError != vr::VROverlayError_None)
        {
            lastError = "CreateDashboardOverlay failed: " + std::to_string(overlayError); 
            return false;
        }

        overlay->SetOverlayWidthInMeters(mainHandle, overlayWidthInMeters);
        overlay->SetOverlayInputMethod(mainHandle, vr::VROverlayInputMethod_Mouse);

        overlay->SetOverlayAlpha(mainHandle, 1.0f);

        vr::VRTextureBounds_t bounds;
        bounds.uMin = 0;
        bounds.uMax = 1;
        bounds.vMin = 0;
        bounds.vMax = 1;
        overlay->SetOverlayTextureBounds(mainHandle, &bounds);

        return true;
    }

    EXPORT void VR_OpenVR_Shutdown()
    {
        if (mainHandle != 0)
        {
            vr::VROverlay()->DestroyOverlay(mainHandle);
            mainHandle = 0;
        }

        vr::VR_Shutdown();
    }

    EXPORT bool VR_OpenVR_SetOverlayTexture(void* texturePtr)// ID3D11Resource
    {
        vr::Texture_t texture = {texturePtr, vr::TextureType_DirectX, vr::ColorSpace_Auto};
        auto error = vr::VROverlay()->SetOverlayTexture(mainHandle, &texture);
        if (error != vr::VROverlayError_None)
        {
            lastError = "SetOverlayTexture failed: " + std::to_string(error); 
            return false;
        }

        return true;
    }

    EXPORT bool VR_OpenVR_SetOverlayRaw(void *buffer, uint32_t width, uint32_t height, uint32_t depth)
    {
        auto error = vr::VROverlay()->SetOverlayRaw(mainHandle, buffer, width, height, depth);
        if (error != vr::VROverlayError_None)
        {
            lastError = "SetOverlayTexture failed: " + std::to_string(error); 
            return false;
        }

        return true;
    }

    EXPORT bool VR_OpenVR_ShowOverlay()
    {
        auto error = vr::VROverlay()->ShowOverlay(mainHandle);
        if (error != vr::VROverlayError_None)
        {
            lastError = "ShowOverlay failed: " + std::to_string(error); 
            return false;
        }

        return true;
    }

    EXPORT bool VR_OpenVR_HideOverlay()
    {
        auto error = vr::VROverlay()->HideOverlay(mainHandle);
        if (error != vr::VROverlayError_None)
        {
            lastError = "HideOverlay failed: " + std::to_string(error); 
            return false;
        }

        return true;
    }

    EXPORT bool VR_OpenVR_IsOverlayVisible()
    {
        return vr::VROverlay()->IsOverlayVisible(mainHandle);
    }

    EXPORT bool VR_OpenVR_PollEvent(uint32_t* eventType)
    {
        vr::VREvent_t e;
        if (vr::VROverlay()->PollNextOverlayEvent(mainHandle, &e, sizeof(e)))
        {
            *eventType = e.eventType;
            return true;
        }

        *eventType = 0;

        return false;
    }
}

Here is the C# wrapper code.

public static class OpenVR_Overlay
    {
        private const string lib = "OpenVR_Overlay.dll";

        [DllImport(lib, CharSet = CharSet.Ansi)]
        public static extern int VR_OpenVR_GetLastError(StringBuilder errorBuffer, int errorBufferLength);

        [DllImport(lib, CharSet = CharSet.Ansi)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_Init(StringBuilder overlayKey, StringBuilder overlayFriendlyName, float overlayWidthInMeters);

        [DllImport(lib)]
        public static extern void VR_OpenVR_Shutdown();

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_SetOverlayTexture(IntPtr texturePtr);

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_SetOverlayRaw(IntPtr buffer, uint width, uint height, uint depth);

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_ShowOverlay();

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_HideOverlay();

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_PollEvent(ref uint eventType);

        [DllImport(lib)]
        [return: MarshalAs(UnmanagedType.I1)]
        public static extern bool VR_OpenVR_IsOverlayVisible();

        private static string GetLastError()
        {
            var value = new StringBuilder(256);
            int length = VR_OpenVR_GetLastError(value, value.MaxCapacity);
            return value.ToString();
        }

        public static bool Init()
        {
            if (!VR_OpenVR_Init(new StringBuilder("VROverlay"), new StringBuilder("VR Overlay"), 1))
            {
                Console.WriteLine(GetLastError());
                return false;
            }

            return true;
        }

        public static void Shutdown()
        {
            VR_OpenVR_Shutdown();
        }

        /*public static bool SetTexture(RenderTexture texture)
        {
            if (!OpenVR_Overlay.VR_OpenVR_SetOverlayTexture(texture.GetNativeTexturePtr()))
            {
                Console.WriteLine(GetLastError());
                return false;
            }

            return true;
        }*/

        public unsafe static bool SetRawBuffer(byte[] buffer, int width, int height, int bytesPerPixel)
        {
            fixed (byte* bufferPtr = buffer)
            {
                if (!VR_OpenVR_SetOverlayRaw(new IntPtr(bufferPtr), (uint)width, (uint)height, (uint)bytesPerPixel))
                {
                    Console.WriteLine(GetLastError());
                    return false;
                }
            }

            return true;
        }

        public static bool Show()
        {
            if (!VR_OpenVR_ShowOverlay())
            {
                Console.WriteLine(GetLastError());
                return false;
            }

            return true;
        }

        public static bool Hide()
        {
            if (!VR_OpenVR_HideOverlay())
            {
                Console.WriteLine(GetLastError());
                return false;
            }

            return true;
        }

        public static bool PollEvent(out uint eventType)
        {
            eventType = 0;
            return VR_OpenVR_PollEvent(ref eventType);
        }
    }

Here is the WPF test renderer.

public partial class MainWindow : Window
    {
        private ContentUserControl control;
        private RenderTargetBitmap renderTarget;
        private byte[] pixelBuffer;

        private DispatcherTimer timer;
        private Stopwatch stopwatch;

        private const int bufferWidth = 300, bufferHeight = 300;

        public MainWindow()
        {
            InitializeComponent();
        }

        protected override void OnContentRendered(EventArgs e)
        {
            base.OnContentRendered(e);

            // create WPF UI element used for rendering
            control = new ContentUserControl();
            control.Measure(new Size(bufferWidth, bufferHeight));
            control.Arrange(new Rect(new Size(bufferWidth, bufferHeight)));

            // create WPF render target to render control content into
            renderTarget = new RenderTargetBitmap(bufferWidth, bufferHeight, 96, 96, PixelFormats.Pbgra32);
            pixelBuffer = new byte[renderTarget.PixelWidth * renderTarget.PixelHeight * 4];

            // render initial image
            RenderImage();

            // init openvr overlay
            if (!OpenVR_Overlay.Init()) return;
            OpenVR_Overlay.SetRawBuffer(pixelBuffer, renderTarget.PixelWidth, renderTarget.PixelHeight, 4);

            // set test/debugging image
            testImage.Source = renderTarget;

            // start update loop
            stopwatch = new Stopwatch();
            stopwatch.Start();

            timer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher);
            timer.Tick += UpdateLoop;
            timer.Interval = TimeSpan.FromMilliseconds(1000 / 60);// 60 fps
            timer.Start();
        }

        private void RenderImage()
        {
            // GPU render image
            renderTarget.Render(control);

            // copy pixels to CPU buffer
            renderTarget.CopyPixels(pixelBuffer, renderTarget.PixelWidth * 4, 0);

            // swap R&B color channels for OpenVR texture format
            for (int i = 0; i < pixelBuffer.Length; i += 4)
            {
                byte r = pixelBuffer[i];
                pixelBuffer[i] = pixelBuffer[i + 2];
                pixelBuffer[i + 2] = r;
            }
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            timer.Stop();
            OpenVR_Overlay.Shutdown();
            base.OnClosing(e);
        }

        private void UpdateLoop(object sender, EventArgs e)
        {
            while (OpenVR_Overlay.PollEvent(out uint eventType))
            {
                //Console.WriteLine("EventType: " + eventType);
                if ((eventType == 200 || eventType == 1705) && stopwatch.ElapsedMilliseconds >= 250)
                {
                    stopwatch.Restart();
                    if (!OpenVR_Overlay.VR_OpenVR_IsOverlayVisible())
                    {
                        Console.WriteLine("Overlay shown!");
                        OpenVR_Overlay.Show();
                    }
                    else
                    {
                        Console.WriteLine("Overlay shown!");
                        OpenVR_Overlay.Hide();
                    }
                }
            }
        }
    }
bddckr commented 5 years ago

For anyone wondering: You can do all of that directly in C# with the provided C# bindings file found in this repo (header directory). No need to marshall back and forth.

I have no issues using SetOverlayTexture with a DX11 texture either.

zezba9000 commented 5 years ago

@bddckr True this is just test code I had laying around. Also just to point it out you can use the bindings and it probably makes more sense in this situational but using a custom lib that does a bunch of stuff at once in C can be faster vs micro invoking methods from C# that cant be inlined. Not that it matters for this but it can actually make stuff easier to for testing (depending) sometimes when all the docs are in C.

Also WPF is D3D9 not D3D11 so you would have to map the DXGI object which didn't care to play around with in this test.