Search code examples
c#uwpxbox

UWP app as an overlay on Xbox console games


I'm trying to develop an FPS counter for games on the Xbox (console). I managed to develop a basic UWP app that counts the FPS using two techniques: simple counting and smoothed average measurement. It displays both results on the screen in the upper left corner (the screenshot below shows the app running on the Xbox Series S in developer mode).

enter image description here

This is the application running on my local machine:

enter image description here

The problem is that I can't find any information in the documentation on how to overlay the counter on top of the game itself, allowing me to play the game while having the FPS counter in the left corner for real-time measurements, similar to what Steam does.

I've read in some places that this is not possible because the Xbox suspends UWP apps when a game is executed to dedicate all the hardware to the game, which makes sense. However, Discord recently arrived on Xbox and it can send notifications that overlay the game when players are speaking on the microphone, also Spotify can play music in the background during gameplay.

Is there any known way to achieve this?

Here is a simplified version of my application that can be run locally for testing if needed:

MainPage.xaml:

<Page x:Class="FPSCounter.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:FPSCounter"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      Background="Transparent">

    <Grid>
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
            <TextBlock x:Name="basicFPSTextBlock" FontSize="24"/>
            <TextBlock x:Name="smoothedFPSTextBlock" FontSize="24"/>
        </StackPanel>
    </Grid>
</Page>

MainPage.xaml.cs:

public sealed partial class MainPage : Page
    {
        private readonly BasicCounterService counterService;
        private readonly SmoothedAverageMeasurementService fpsCounter;

        public MainPage()
        {
            InitializeComponent();
            SetWindowPositionAndSize();
            counterService = new BasicCounterService();
            fpsCounter = new SmoothedAverageMeasurementService(0, 0.9);
            StartFPSCounter();

        }

        private void SetWindowPositionAndSize()
        {
            ApplicationView.GetForCurrentView();

            double desiredWidth = 50;
            double desiredHeight = 50;

            ApplicationView.GetForCurrentView().SetPreferredMinSize(new Size(desiredWidth, desiredHeight));
            ApplicationView.GetForCurrentView().TryResizeView(new Size(desiredWidth, desiredHeight));
        }

        private async void StartFPSCounter()
        {
            while (true)
            {
                int basicFPS = await counterService.CalculateFPS();
                double smoothedFPS = await fpsCounter.CalculateSmoothedAverage();
                UpdateFPSUI(basicFPS, smoothedFPS);
            }
        }

        private async void UpdateFPSUI(int basicFPS, double smoothedFPS)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                basicFPSTextBlock.Text = $"Basic FPS: {basicFPS}";
                smoothedFPSTextBlock.Text = $"Smoothed FPS: {Math.Round(smoothedFPS)}";

                if (ApplicationView.GetForCurrentView().ViewMode == ApplicationViewMode.CompactOverlay)
                {

                }
                else
                {

                }
            });
        }
    }

BasicCounterService.cs:

internal class BasicCounterService
    {
        private readonly Stopwatch stopwatch;
        private int frameCount;

        public BasicCounterService()
        {
            stopwatch = new Stopwatch();
            frameCount = 0;
        }

        public async Task<int> CalculateFPS()
        {
            stopwatch.Start();

            while (stopwatch.Elapsed.TotalSeconds < 1)
            {
                await Task.Delay(16);
                frameCount++;
            }

            stopwatch.Stop();
            int fps = frameCount;

            frameCount = 0;
            stopwatch.Reset();

            return fps;
        }
    }

SmoothedAverageMeasurementService.cs:

internal class SmoothedAverageMeasurementService
    {
        private readonly double smoothing;
        private double measurement;
        private readonly Stopwatch stopwatch;
        private int frameCount;

        public SmoothedAverageMeasurementService(double initialMeasurement, double smoothingFactor)
        {
            measurement = initialMeasurement;
            smoothing = smoothingFactor;
            stopwatch = new Stopwatch();
            frameCount = 0;
        }

        public async Task<double> CalculateSmoothedAverage()
        {
            stopwatch.Start();

            while (stopwatch.Elapsed.TotalSeconds < 1)
            {
                await Task.Delay(16);
                frameCount++;
            }

            stopwatch.Stop();
            double currentMeasurement = frameCount;

            measurement = (measurement * smoothing) + (currentMeasurement * (1.0 - smoothing));

            frameCount = 0;
            stopwatch.Reset();

            return Math.Round(measurement);
        }
    }

Solution

  • You cannot, for the reasons you described. While a UWP can run in the background, it has no access to the screen then, so cannot overlay with a game.