Implementing Multi-Window Setup for Windows UWP

We’re currently working on multi monitor fitness simulation and require a multi-window configuration for the Windows UWP target platform which unfortunately doesn’t support Multi-window natively. Our project relies onmultiple monitors and Bluetooth sensor integration with Bluetooth code that’s tailored specifically for Windows UWP, and unfortunately, it doesn’t function properly when applied to other target platforms.

I’ve explored Unity’s official documentation on multi-display setups (Unity - Manual: Multi-display), but it appears that this method isn’t compatible with Windows UWP , as discussed in this forum thread: how to support multiple displays UWP . According to the responses there, there appear to be three potential solutions and we found 2 more that we’re considering:

  • One option is to acquire an asset designed for multi-monitor support in Windows UWP, such as the one available at (Unity Assets) as posted here. However, this solution raises some concerns since it’s not on the asset store and could be a bit shady. (Questionable source.)

  • Another possibility is to create our own secondary monitor functionality using Windows API, a more hands-on approach that would require quite some coding. as described here . (Maintenance Challenge)

  • We’re also contemplating the idea of rworking the Bluetooth code to function beyond the limitations of Windows UWP, potentially utilizing resources like GitHub - adabru/BleWinrtDll: BLE for Unity 3d in Windows without UWP. or insights from this thread: bluetooth low energy on windows under unity . (Strategic Shift)

  • Another approach is using Nvidia Surround alongside viewport rect with dual cameras to simulate a second monitor.

  • Lastly, we’re considering the option of developing a separate application specifically designed as a secondary monitor, which could communicate with the main application via UDP or another suitable method. (Communication and Sync Issue)

Given that the most of the referenced threads above date back to 2013, there’s a possibility that there’s now a different, built-in solution available.

We need your guidance: Are we overlooking any potential approaches, or is there a recommended solution among these? Your insights would be greatly appreciated.

This isn’t entirely accurate. Since that response that I gave 7 years ago, a lot changed. The MultiDisplay feature should be functional on UWP. The only limitation is that it doesn’t allow programmatic positioning of spawned windows, which might or might not be a blocker for you.

That said, using option 4 would probably be the most performant and ideal if you control the environment setup.

In my testing, I encountered some issues. The 2. Window stopped responding, and didn’t allow for interaction such as resizing. However, it’s worth considering that these problems might be stemming from other factors. I’m going to delve further into this further to identify the root causes.

Okay, doing some further testing, It seems to be a problem with my Bluetooth code script and the 2. Screen interacting. It worked fine on the main screen, but when the script is running on a 2. Screen it doesn’t work anymore and I keep getting.

All times must be in 0-1 range
And
A breakpoint instruction (__debugbreak() statement or a similar call) was executed in SBP2.exe.

Into

UnityPlayer_UAP_x64_debug_il2cpp.pdb not loaded

I’ve tried about everything I can think of, but can’t pinpoint the error.

You can set up symbol loads from our symbol server (https://symbolserver.unity3d.com). Do you have the callstack of the crash and example code that causes the crash?

This is the call stack. I sadly can’t figure out what part of the code is the root cause of the crash. The problem is it’s only crashing when the code runs on 2. Screen and my attempts of figuring out why haven’t been very successful. Also, trying to use the symbol server just gives me “source not available” instead of UnityPlayer_UAP_x64_debug_il2cpp.pdb not loaded.

UnityPlayer.dll!00007ffa24477401() Unknown
UnityPlayer.dll!00007ffa22fbe6ae() Unknown
UnityPlayer.dll!00007ffa2300ea2e() Unknown
UnityPlayer.dll!00007ffa23ebcaca() Unknown
UnityPlayer.dll!00007ffa281658c9() Unknown
UnityPlayer.dll!00007ffa2446108a() Unknown
UnityPlayer.dll!00007ffa2445e162() Unknown
UnityPlayer.dll!00007ffa2445e69b() Unknown
UnityPlayer.dll!00007ffa244640e0() Unknown
UnityPlayer.dll!00007ffa252e916f() Unknown
kernel32.dll!BaseThreadInitThunk() Unknown
ntdll.dll!RtlUserThreadStart() Unknown

this is how the call stack looks after setting the symbol server:

>    UnityPlayer.dll!Gradient::EvaluateHDR<0>(struct math::_float4 const &)    Unknown
     UnityPlayer.dll!Gradient::Evaluate<0>(float)    Unknown
     UnityPlayer.dll!Gradient::Evaluate(float)    Unknown
     UnityPlayer.dll!Build3DLine<unsigned short>(unsigned char *,unsigned short *,struct LineParameters const &,struct math::float4x4 const &,struct math::float4x4 const &,struct math::float3_storage const *,float const *,unsigned __int64,unsigned __int64,unsigned __int64,bool,float,bool,float)    Unknown
     UnityPlayer.dll!LineRenderer::RenderGeometryJob(struct SharedGeometryJobData *,unsigned int)    Unknown
     UnityPlayer.dll!ujob_execute_job()    Unknown
     UnityPlayer.dll!lane_guts()    Unknown
     UnityPlayer.dll!lane_run_loop()    Unknown
     UnityPlayer.dll!worker_thread_routine()    Unknown
     UnityPlayer.dll!Thread::RunThreadWrapper(void *)    Unknown
     kernel32.dll!BaseThreadInitThunk()    Unknown
     ntdll.dll!RtlUserThreadStart()    Unknown

this is the whole script code, but i’m not entirely sure this in itself causes the crash or not. Removing it however does stop it from crashing. It sees to be able to do one loop then crash after if it’s on 2. screen but work fine if I use the same script on the main screen

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Assets.Scripts.Bluetooth.Connection_and_Device_Management;
using System;
using Assets.Scripts.Bluetooth.BluetoothEventArgs;
using System.Linq;
using System.Collections.Concurrent;
using TMPro;

public class LineRendererSetter : MonoBehaviour
{
#if WINDOWS_UWP
    private ConcurrentQueue<float> hearthRateQueue;
#else
    private System.DateTime throttle; //test
#endif
    private LineRenderer lineRenderer;
    private float height;
    private RectTransform rectTransform;
    private int amountPositions = 50;
    private List<float> hearthRate; // this could be change to a threadsave list to get rid of the queue but that doesn't seem to exist currently
    private TextMeshProUGUI AvgText;
    private TextMeshProUGUI MaxText;
    private TextMeshProUGUI VNumberLow;
    private TextMeshProUGUI VNumberHigh;
    private TextMeshProUGUI CurrentText;
    void Start() // sets all the required values
    {
        //textSetup();
        hearthRate = new List<float>();
        lineRenderer = GetComponent<LineRenderer>();
        lineRenderer.positionCount = amountPositions; // position amount
        rectTransform = GetComponent<RectTransform>();
        height = rectTransform.sizeDelta.y;
        calculatePositions(); // start calculating positions
#if WINDOWS_UWP // setup bluetooth values
        hearthRateQueue = new ConcurrentQueue<float>();
        DeviceDataHandler.Instance.HeartRateSensors += HeartRateSensorEvent;
#else // setup editor values
        throttle = DateTime.Now; //test
#endif
    }

    private void textSetup()
    {
        AvgText = transform.parent.Find("AVG").Find("ValueHolder").Find("Value").GetComponent<TextMeshProUGUI>();
        MaxText = transform.parent.Find("MAX").Find("ValueHolder").Find("Value").GetComponent<TextMeshProUGUI>();
        VNumberLow = transform.parent.Find("VNumber Low").GetComponent<TextMeshProUGUI>();
        VNumberHigh = transform.parent.Find("VNumber High").GetComponent<TextMeshProUGUI>();
        CurrentText = transform.parent.Find("Current Value").Find("Value").GetComponent<TextMeshProUGUI>();
    }
    // Update is called once per frame
    void Update()
    {
#if WINDOWS_UWP //bluetooth code
        if (!hearthRateQueue.IsEmpty) // empty the queue if needed
        {
            float Rate;
            if (hearthRateQueue.TryDequeue(out Rate))
            {
                HearthRate(Rate);
                if (CurrentText)
                {
                    CurrentText.SetText(Rate.ToString());
                }
            }
            UpdateLineData();
        }
#else // editor code
        if (DateTime.Now > throttle.AddSeconds(1.0)){
            throttle = throttle.AddSeconds(1.0);
            float Rate = UnityEngine.Random.Range(0, 200);
            HearthRate(Rate);
    //        if (CurrentText){
    //            CurrentText.SetText(Rate.ToString());
     //       }
            UpdateLineData();
        }
#endif
    }
#if WINDOWS_UWP
    private void HeartRateSensorEvent(object sender, BLEushortEventArgs deviceData)
    {
        Debug.Log("HearthRateDebug Adding"+ deviceData.Value);
        hearthRateQueue.Enqueue(deviceData.Value);
    }
#endif
    private void HearthRate(float DataValue) // manages the list
    {
        {
            if (hearthRate.Count < amountPositions) // if still space add values
            {
                hearthRate.Add(DataValue);
            }
            else
            {
                float lastRate = 0;
                for (int i = hearthRate.Count - 1; i >= 0; i--) // if no more free space iterate over the list in reverse and reorder values -> FIFO
                {
                    if (lastRate == 0)
                    {
                        lastRate = hearthRate[i];
                        hearthRate[hearthRate.Count - 1] = DataValue;
                    }
                    else
                    {
                        float Rate = hearthRate[i];
                        hearthRate[i] = lastRate;
                        lastRate = Rate;
                    }
                }
            }
        }
    }

    /// <summary>
    /// Calculates the Position for all lineRenderer positions. This assumes all data object have the same time inbetween them.
    /// </summary>
    private void calculatePositions()  // this assumes all data object have the same time in between them. If this is not the case for the bluetooth data provided this needs to be changed
    {
        float width = rectTransform.sizeDelta.x;
        for (int i = 0; i < lineRenderer.positionCount; i++) // build positions in x direction
        {
            Vector3 Position = lineRenderer.GetPosition(i);
            float x = i < amountPositions / 2 ? -((float)amountPositions / 2 - i) / (amountPositions / 2) : (i - amountPositions / 2) / ((float)amountPositions / 2);
            Position.x = x * width / 2;
            Position.z = 1;
            lineRenderer.SetPosition(i, Position);
        }
    }

    /// <summary>
    /// Updates the LineRenderer to show the provided Data. This assumes all data object have the same time inbetween them.
    /// </summary>
    public void UpdateLineData() // this assumes all data object have the same time in between them. If this is not the case for the bluetooth data provided this needs to be changed
    {
        /*
        if (lineRenderer.positionCount != data.Count){ // adjust amount of positions if neccesary (this looks visually more appealing but can create other problems
            lineRenderer.positionCount = data.Count;
            calculatePositions();
        }*/
        for ( int i = 0; i < hearthRate.Count; i++)
        {
            Vector3 Pos = lineRenderer.GetPosition(i);;
            float calculatedPosition = (hearthRate[i] - hearthRate.Min()) * height / (hearthRate.Max()-hearthRate.Min());
            Pos.y = calculatedPosition-height/2;
            lineRenderer.SetPosition(i, Pos);
        }
       //SetTexts();
    }
    /// <summary>
    /// Sets the texts adjacent to the LineRenderer
    /// </summary>
    private void SetTexts()
    {
        AvgText.SetText(Math.Round(hearthRate.Average()).ToString());
        MaxText.SetText(hearthRate.Max().ToString());
        VNumberLow.SetText(Math.Round(hearthRate.Max()/3).ToString());
        VNumberHigh.SetText(Math.Round(hearthRate.Max()/ 3*2).ToString());
    }


}

It also crashes if i change the code to the editor only code so it doesn’t sem to be the bluetooth interaction that is the problem

Looking at the callstack, it suggests to me that the line renderer component is crashing, not your bluetooth code. And your script does use a line renderer!

Does getting rid of line renderer make the crash go away?

Yes, it does get rid of the Crash. Oddly enough, it only happens if the line renderer script is on the 2. Screen however, which is super odd. If I have it on the main screen, it works.

Are you able to hardcode the inputs that you pass to line renderer and make it crash without any bluetooth activity? We’d be interested in a bug report so we could address it, but it might be tricky to investigate if it requires a bluetooth device like yours.

Yes i’m able to reliably crash the game using this (hopefully) minimal example without any bluetooth code.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Animations.Rigging;

public class LineRendererMinimalTest : MonoBehaviour
{
    private LineRenderer lineRenderer;
    private DateTime throttle; //test
    private int amountPositions = 50;
    private List<float> hearthRate;
    private RectTransform rectTransform;
    private float height;
    // Start is called before the first frame update
    void Start()
    {
        hearthRate = new List<float>();
        lineRenderer = GetComponent<LineRenderer>();
        lineRenderer.positionCount = amountPositions;
        rectTransform = GetComponent<RectTransform>();
        height = rectTransform.sizeDelta.y;
        calculatePositions();
        throttle = DateTime.Now;
    }

    void Update()
    {
        if (DateTime.Now > throttle.AddSeconds(0.2))
        {
            throttle = throttle.AddSeconds(0.2);
            float Rate = UnityEngine.Random.Range(0, 200);
            HearthRate(Rate);
            UpdateLineData();
        }
    }

    private void HearthRate(float DataValue) // manages the list
    {
        {
            if (hearthRate.Count < amountPositions) // if still space add values
            {
                hearthRate.Add(DataValue);
            }
            else
            {
                float lastRate = 0;
                for (int i = hearthRate.Count - 1; i >= 0; i--) // if no more free space iterate over the list in reverse and reorder values -> FIFO
                {
                    if (lastRate == 0)
                    {
                        lastRate = hearthRate[i];
                        hearthRate[hearthRate.Count - 1] = DataValue;
                    }
                    else
                    {
                        float Rate = hearthRate[i];
                        hearthRate[i] = lastRate;
                        lastRate = Rate;
                    }
                }
            }
        }
    }
    public void UpdateLineData()
    {
        for (int i = 0; i < hearthRate.Count; i++)
        {
            Vector3 Pos = lineRenderer.GetPosition(i); ;
            float calculatedPosition = (hearthRate[i] - hearthRate.Min()) * height / (hearthRate.Max() - hearthRate.Min());
            Pos.y = calculatedPosition - height / 2;
            lineRenderer.SetPosition(i, Pos);
        }
    }

    private void calculatePositions()  // this assumes all data object have the same time in between them. If this is not the case for the bluetooth data provided this needs to be changed
    {
        float width = rectTransform.sizeDelta.x;
        for (int i = 0; i < lineRenderer.positionCount; i++) // build positions in x direction
        {
            Vector3 Position = lineRenderer.GetPosition(i);
            float x = i < amountPositions / 2 ? -((float)amountPositions / 2 - i) / (amountPositions / 2) : (i - amountPositions / 2) / ((float)amountPositions / 2);
            Position.x = x * width / 2;
            Position.z = 1;
            lineRenderer.SetPosition(i, Position);
        }
    }
}

and this is the matching stack trace

>    UnityPlayer.dll!Gradient::EvaluateHDR<0>(struct math::_float4 const &)    Unknown
     UnityPlayer.dll!Gradient::Evaluate<0>(float)    Unknown
     UnityPlayer.dll!Gradient::Evaluate(float)    Unknown
     UnityPlayer.dll!Build3DLine<unsigned short>(unsigned char *,unsigned short *,struct LineParameters const &,struct math::float4x4 const &,struct math::float4x4 const &,struct math::float3_storage const *,float const *,unsigned __int64,unsigned __int64,unsigned __int64,bool,float,bool,float)    Unknown
     UnityPlayer.dll!LineRenderer::RenderGeometryJob(struct SharedGeometryJobData *,unsigned int)    Unknown
     UnityPlayer.dll!ujob_execute_job()    Unknown
     UnityPlayer.dll!lane_guts()    Unknown
     UnityPlayer.dll!lane_run_loop()    Unknown
     UnityPlayer.dll!worker_thread_routine()    Unknown
     UnityPlayer.dll!Thread::RunThreadWrapper(void *)    Unknown
     kernel32.dll!BaseThreadInitThunk()    Unknown
     ntdll.dll!RtlUserThreadStart()    Unknown

Can you submit that script with a small scene that’s configured to make it crash as a bug report as outlined here? Unity QA: Building quality with passion. Forums are not great at tracking bug reports - submitting it via the bug reporter will allow you to subscribe to fix notifications, add the issue to the issue tracker, etc.

Sure thing. Created a very minimal example in a new project that should ease your process of reproducing the issue. If you need anything else, please let me know.

Thanks, can you tell me the bug report number you received in the email?

IN-54251

This looks like an assertion failure. The bug has been confirmed. In the meantime, you can work around it by simply changing the build configuration to Release in Visual Studio.