Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Today's Best Practices for Migrating Windows apps to Windows on Arm

28 Jan 2021 1  
In this article, we'll demonstrate the performance hit a sample application takes under emulation, and demonstrate how to port an existing codebase to Windows on Arm. We will show how to set up your development environment to target the ARM64 processor using .NET framework 4.8.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

In this article, we'll demonstrate the performance hit a sample application takes under emulation, and demonstrate how to port an existing codebase to Windows on Arm. We will show how to set up your development environment to target the ARM64 processor using .NET framework 4.8.

We are starting to see adoption of Windows 10 for Arm accelerate through many new devices available in the market, running on Arm-based devices such as Microsoft Surface Pro X, Samsung Galaxy Book S, and Lenovo Yoga 5G devices. Windows 10 on Arm provides full support for running x86 win32 apps, including Windows Forms apps and Windows Presentation Foundation (WPF) apps. The main advantage is the amazing battery life of the Arm processor. The device that I use for this article is a Surface Pro X running on an ARM64 processor.

While these apps do run under Windows 10 on Arm, an application compiled for Intel processors runs on these devices under x86 and X64 emulation layers. Applications running on emulation are, of course, slower than natively running ARM64 applications.

In this article I’ll start with a WPF application that performs many floating-point calculations. We can compile this for x86 or x64. When the application is executed on the Surface Pro X, it will run under the x86 emulation.

Then I’ll demonstrate how to port this application to a UWP application and deploy this UWP application to be run on an ARM64 processor.

Finally, I'll compare the runtime performance results between the WPF app running under emulation and the UWP running natively on Windows 10 on Arm.

For this article, I assume that you have .NET knowledge (the code is in C#), and some knowledge of WPF, UWP, and XAML. All the sample code can be found at GVerelst/Mandelbrot (github.com).

The Emulation Layer

Applications that are compiled for the .NET Framework can run on an ARM64 Windows system, but the code will be executed in an emulator. This allows Windows 10 on Arm to run almost any code, but not in the most optimal way.

For an x86 application, there is no difference from running on x86 Windows. The x86 system DLLs then go through the Windows on Windows (WoW) application layer. This layer contains an x86-to-Arm CPU emulator that will make the x86 code run on the Arm processor. After that, the normal execution continues to the system services and the kernel.

The x86 instructions are translated at runtime to ARM64 and are cached on disk for further use. This means there is no special installation required. The amount of non-native code is limited as much as possible, which explains why this can still run reasonably fast. But this will not run as fast as a native ARM64 application. To learn more, see How x86 emulation works on ARM in the Microsoft Windows Developer documentation.

If you can compile binaries for the Arm processor, however, they can talk directly to the native system DLLs. These in turn talk to the system services, and from there the kernel takes over if needed. No emulations needed. This clearly is the optimal scenario.

Architecture of the Sample Application

To see the difference in performance, we need an application that does a lot of computations. To make it graphically appealing, I chose a representation of the famous Mandelbrot set. The Wiki article explains exactly how the points are calculated, but here is what is important for us to know:

Each point on the screen is calculated by executing a small function repeatedly on the point until either the distance of the point to the origin becomes bigger than 2, or the maximum number of iterations has been reached. The points that don’t "escape" from the 2 unit circle after the maximum number of iterations has been reached are in the Mandelbrot set, and the other points are not. The number of times that the function is called for a point before it "escapes" will determine its color.

The important point here is that, for each point, a lot of floating-point calculations need to be performed to determine its color. This is what the application looks like:

Image 1

More iterations mean more processing time. In this case we used 5000 iterations, which took 3.824 seconds on my development PC. We will use this for our benchmarks. We only time the actual calculations, not the time to output it to a bitmap and display it.

Each point on the orange surface needs to be calculated, so the number of calculations also depends on the bitmap size as well. It is like a hidden parameter.

I created a separate Mandelbrot library for the calculations. This library is shared between both projects, so the code executed for the calculations is exactly the same.

The WPF application uses this library and consists of a main window with a drawing area and some controls. Clicking on the Start Calculation button calls the calculation function and outputs the result.

The UWP application will do the same, with some changes due to the nature of UWP.

For this project I used the latest version of Visual Studio 2019 and .NET Framework 4.8.

In the VS2019 Installer, verify that you have installed the ".NET desktop development" and the "Universal Windows Platform development" workloads.

Image 2

Building the x86 WPF Application

The initial application is a WPF application using the Mandelbrot library that I created separately. We can run it on the Surface. It is not specifically compiled for ARM64, so it will run in x86 emulation mode.

To deploy the WPF application, we just have to compile the solution for "Any CPU," then copy the app’s bin folder containing the executable and DLLs to a folder on the Surface.

Image 3

In the current .NET Framework 5, it is not possible to compile a WPF application directly for ARM64. Microsoft plans to include WPF ARM64 support in a future release of .NET 5 or possibly .NET 6.

For now, we can use UWP to create an application that will compile for ARM64. We are going to create a new UWP project in our solution using .NET Framework 4.8. A UWP application can be published as a native ARM64 application, which runs without emulation.

In the UWP application, we will reuse as much of the WPF code as possible.

The process to create a new UWP application is quite simple:

  1. Right-click on the solution.
  2. Select Add > New Project…
  3. In the search box at the top of the window, type "UWP".
  4. Select Blank App (Universal Windows).
  5. Select the project in the right language. In this article I will be using C#.
  6. Click Next. On the next page, give the project a name (Mandelbrot.UWP), keep the default location and click Create.

    Image 4

When we run this project, it displays an empty window.

Compiling the Project for ARM64

Before we start to code, let’s publish the project and install it on the Surface. When we run the project, it should run under ARM64 native mode.

First, right-click the UWP project, then select Publish > Create App packages…

On the first page, we select the distribution method. Select Sideloading while we’re developing. The other option is Microsoft Store, which brings other challenges with it. See my article about this on msdev.pro. Keep Enable automatic updates unchecked and click Next.

Image 5

On the Signing Methods page, keep the defaults and click Next.

On the last page use the following settings:

Image 6

I only selected ARM64 because this option is mutually exclusive with the other options. You cannot select ARM64 together with other options on this page.

Finally, click Create. The project is now compiled for ARM64, and the package files are created in c:\temp\deploy.

When the package, check the contents of the folder. Notice that we have now created a package with debug info in it because we didn’t set the configuration to Release. For now, this is okay. For the benchmarking, we will use Release mode.

Image 7

Deploying the Package on the Surface

On the Surface Pro X, copy the version that was created in the Deploy folder to a folder that can be accessed by the Surface. I used OneDrive, but you can use whatever method you like. A simple USB drive works.

Before we can install the package, we need to put the Surface into Developer Mode:

In the Windows search box (next to the Start button) type "developer settings" and then open the settings. This will take you to the "Windows Security" settings page.

Switch Developer Mode on, so you can install UWP applications directly on the Surface without having to go through the Windows Store.

Image 8

Once that’s done, go to the shared folder and start the Install.ps1 script. This will perform the necessary steps to deploy the empty application.

We can now start the application from the Windows Start menu. When the application is started, start Task Manager. Notice that the UWP application runs natively — no emulation! The WPF version runs in 32-bit emulation mode.

Image 9

Finishing the UWP Version of the Mandelbrot Application

Now that we have proved that the UWP version runs in Native mode on the Surface, we can implement the same functionality as in the WPF version. We’ll use the same calculation library in both projects. The timing will be done only on the calculations in the library.

We start from the WPF application that has the following structure:

Image 10

A UWP application’s user interface is XAML-driven, so most of the code will be reusable. Not all the APIs are the same, though. In the WPF application, I used the MVVM pattern to split the functionality from the representation. This will be rewarded now during the conversion.

In Mandelbrot.UWP, reference the Mandelbrot.Calculations project by right-clicking References, then select Add reference… Select the Mandelbrot.Calculations checkbox to include the project.

Image 11

Next, create a folder called ViewModels under the UWP project by right-clicking on the project, then select Add > New Folder. Repeat this to create a folder called Extensions as well.

Copy the file MandelbrotParameters.cs from Mandelbrot.WPF/ViewModels to Mandelbrot.UWP/ViewModels.

For code cleanliness, fix the namespace by changing it to Mandelbrot.UWP.ViewModels. This is not mandatory, but is advised.

The WPF application runs inside a WPF Window, but the UWP app runs inside a page, so we must fix the datatype:

class MandelbrotParameters : INotifyPropertyChanged
{
    private readonly MainPage _window;
    public MandelbrotParameters(MainPage window)
    {
        _window = window;
        Reset();
    }
// ...

You may want to change the name _window to _page, but to keep the two code bases similar I decided not to do this. I could have started from a WPF application using a Page instead of a Window as well. But this is a minor conversion step.

Copy RelayCommand.cs to the UWP project. Fix the namespace as well.

Now let’s fix the XAML. We cannot just copy the XAML file from the WPF project because it describes a Window. We can, however, copy the main XAML element under the <Window> element. So select the top <Grid> element in WPF and use it to replace the top <Grid> element in UWP.

UWP doesn’t know the <Label> element, so let’s replace all the <Label> elements with <TextBlock> elements. Do this for every <Label>:
 

WPF Version

<Label Content="Top:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Margin="0,0,0,5" HorizontalAlignment="Right"
/>

UWP Version

<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Margin="0,0,0,5" HorizontalAlignment="Right" >
Top: </TextBlock>

In UWP, data binding is by default OneWay. That means that if you set the value of a property bound to a control in code, it will be reflected in the page, but if you change the value of the control in the UI, its value will not be sent back to the bound property. Each {Binding xxx} element now needs to become {Binding xxx, Mode=TwoWay}. Here is the example for the top TextBox:

<TextBox HorizontalAlignment="Left" VerticalAlignment="Center" Width="51" Grid.Row="1" Grid.Column="1" Margin="0,0,0,5" Text="{Binding Top, Mode=TwoWay}"/>

In the MainPage class, set the DataContext to a new MandelbrotParameters instance. We pass this as a parameter to give our ViewModel the possibility to call the DrawImage method.

public MainPage()
{
    this.InitializeComponent();
    DataContext = new MandelbrotParameters(this);
}

Copy the DrawImage method from the WPF MainWindow.cs to UWP MainPage.cs. Here we see an example of the fact that the UWP APIs are not always the same as the WPF APIs: the WriteableBitmap constructor doesn’t take six parameters, but only two: the width and the height. The fix is easy: remove all the other parameters.

Copy GetImageViewPort as well. This method requires no changes.

Copy WriteableBitmapExtensions.cs from the Extensions folder. You’ll see compilation errors because the bitmap APIs in UWP are slightly different from their WPF counterparts. Replace the SetPixels methods:

Here's the original WPF version:

public static void SetPixels(
    this WriteableBitmap wbm,
    IEnumerable<FractalPoint> pts)
{
    wbm.Lock();
    IntPtr buff = wbm.BackBuffer;
    int Stride = wbm.BackBufferStride;

    unsafe
    {
        byte* pbuff = (byte*)buff.ToPointer();

        foreach (FractalPoint pt in pts)
        {
            System.Drawing.Color c = pt.Color;
            int loc = pt.Point.Y * Stride + pt.Point.X * 4;
            pbuff[loc] = c.B;
            pbuff[loc + 1] = c.G;
            pbuff[loc + 2] = c.R;
            pbuff[loc + 3] = c.A;
        }
    }

    wbm.AddDirtyRect(new Int32Rect(0, 0, (int)wbm.Width, (int)wbm.Height));
    wbm.Unlock();
}

The new UWP version should look like this:

public static void SetPixels(
    this WriteableBitmap wbm,
    IEnumerable<FractalPoint> pts)
{
    byte[] imageArray = new byte[(int)(wbm.PixelWidth * wbm.PixelHeight * 4)];
    int i = 0;
    foreach (var p in pts)
    {
        imageArray[i] = p.Color.B;
        imageArray[i + 1] = p.Color.G;
        imageArray[i + 2] = p.Color.R;
        imageArray[i + 3] = p.Color.A;

        i += 4;
    }
    using (Stream stream = wbm.PixelBuffer.AsStream())
    {
        //write to bitmap
        stream.Write(imageArray, 0, imageArray.Length);
    }
}

Deploying Both Applications to the Surface

Now we deploy both applications to the surface so that we can compare performance.

Make sure you use Release mode and rebuild the solution.

Perform the steps that we took before to deploy the UWP application to the Surface. Do the same for the WPF project.

Was it worth all the trouble to make the application run natively in Arm 64-bit code? Let’s see.

To calculate the Mandelbrot points, I maximized the application to make sure that we use the same number of calculations for the comparison, and I kept the default parameters, except for the iteration count, which was set to 5000.

The WPF application took 35.671 seconds to calculate the Mandelbrot points.

The UWP application took only 3.203 seconds! That is about 11 times faster.

Wrapping Up

Developing for Native mode is totally worth the trouble. I started from a WPF application, in which the responsibilities were nicely separated. This was obtained by using the MVVM pattern, and splitting out the code for the bitmap generation, and the calculations. Thanks to this, the modifications to be made in the UWP applications were minimal.

I didn’t explore how this behaves when using parallelism. The points are all calculated sequentially. It may be interesting to see if this would change the results and if parallelism changes a lot for the performance.

To learn more, here are some code samples and tutorials digging into a few of the topics behind the code in our example application.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here