Getting high with DPI and Windows::UI::Xaml::Controls::SwapChainPanel

I've been making some bad choices lately. Deleting code first, not debugging properly, not paying attention. The resulting frustration was the universe reproaching me for "messing with things you dinnae understand" (as a well-known starship engineer used to say, IIRC). Here's the skinny.

I was looking at the "DirectX and XAML App" project type in Visual Studio and thinking wow, this is pretty cool. I already had WinRT-without-XAML Direct3D/2D source code that works for Windows and Windows Phone. And in fact the same source builds for Win32, too, using conditional compilation. And there's even a run-as-fast-as-you-can switch for Win32, which doesn't wait for Present() (but you have to set up your swap chain differently for that to happen). So, I already had three different ways of creating a swap chain in there: vanilla for full-speed, ForHwnd for Win32, and ForCoreWindow for WinRT. So I thought what's another swap chain? Let's get ForComposition in there, too.

For me, since I already had so much working code, there was no sense in starting in the project template code and refactoring it into the form I was using. There were too many things the project template was doing differently. I couldn't avoid the fact that it uses a totally different app model, but the code uses the XAML UI thread to create the swap chain (etc) and render, and does independent input on another thread. My code wouldn't be as general if I changed it to work that way, and I already have independent input (so, no need for CreateCoreIndependentInputSource). Instead, then, I added WinRT-with-XAML facilities to my code and lit them up via an INTEGRATING_WITH_XAML preprocessor identifier that I defined. Under that switch, I call CreateSwapChainForComposition, I point the SwapChainPanel in the XAML UI at my swap chain, I don't pump messages, etc, etc.

There is code in the project template that handles the SwapChainPanel's composition scale. That looked like it was needed if you were going to pinch/zoom or otherwise transform the panel. I wasn't: the panel was just going to stay put and fill the page. So I didn't use that stuff. That was one mistake.

Everything was fine until I started high-DPI testing. Windows Store apps don't respond to the desktop DPI setting---instead, the user goes to PC settings > PC and devices > Display. Then, under More options, is a setting labeled "Change the size of apps, text, and other items on the screen (only applies to displays that can support it)". Windows Store apps (with or without XAML) use that setting, and they respond to the DPI-changed event that it raises. My WinRT-without-XAML apps built from my codebase work fine with that setting. My WinRT-with-XAML apps built from the same codebase did not. The XAML was fine: scaled up but re-laid out, so it fit the screen nicely. The stuff going through my swap chain was scaled up and, it looked like, off-screen. So I set about finding out why.

I spent a long time trying different things in my code, but I came up empty every time. My swap chain was being scaled off-screen, and that reminded me of what the OS does for non-DPI-aware non-WinRT apps, so I even resorted to tweaking my manifest as if I were a Win32 app, to no avail.

So I went back to the project template code and started debugging with breakpoints to see exactly what happens when you change that DPI setting, and what I'd missed. The first thing that happens is a DpiChanged event is handled, and the project template code stores that value and then recreates the swap chain (etc). The swap chain is set to the size of the SwapChainPanel multiplied by its composition scale. The DPI value isn't a factor at all, although it is handed to Direct2D, which needs it because D2D always works in DIPs. I could see that the size was the size of the screen in pixels, and the scale was 1. That made sense: my code, too, sets swap chain size in pixels (not DIPs), and there was no transform I could see happening to the SwapChainPanel. So it made sense, but it didn't help. My second bad choice was to stop the debugger. With my limited imagination, it didn't occur to me that another event, and another recreation of the swap chain (etc), was about to happen.

But eventually I went back into the debugger and discovered that in fact the DpiChanged is followed by a SwapChainPanelSizeChanged event and the code recreates the swap chain (etc) a SECOND time for the same user action. Hmm. This time, while the composition scale is still 1 (again, no transform happening that I could see), the swap chain is now equal to the screen size in DIPs. It seemed, then, that the project template code creates the swap chain in DIPs, but every code path in my code sets it in raw pixels every time without fail. So I stopped debugging (another bad choice) and changed my code to use DIPs for the with-XAML case. And of course that didn't work, either: yet another bad choice.

Back into the debugger. Guess what, there's actually a third event, a CompositionScaleChanged. And guess what, the swap chain (etc) is recreated a THIRD time for the same user action. This time, the SwapChainPanel still matches the screen size in DIPs, but this time the composition scale is greater than one and so it multiplies the DIPs back up to raw pixels. This time I'd learned from my mistake and I kept hitting F5 to make sure that really was the last of the events.

It was, so I changed my code back to using raw pixels as it had been all along. It was 4am and the answer was right in front of me, but my head was focused on swap chain dimensions and I completely missed the significance of that composition scale value that had finally changed to something other than 1. It wasn't until the next day that I noticed code in the project template that applies a downscale transform to the swap chain equal to the reciprocal of the SwapChainPanel's composition scale value. Evidently, a high-DPI setting causes the SwapChainPanel's contents to be scaled up, therefore you have to scale down the stuff you render inside it to compensate. As I say, I hadn't used any composition scale code from the project template---misunderstanding the full scope of what it's used for---so I'd lost the bit that makes high-DPI work. But I was able to fix the issue with that knowledge.

That cascade of swap chain recreations in the project template code is interesting. The first time through, the exact same swap chain is recreated (redundantly), but D2D is given the correct DPI. The second time through, the swap chain is resized incorrectly (temporarily) and nothing good happens. The third time through, the swap chain is set back to the size it's been all along, but nothing else good happens. That kind of thing is for the most part an unavoidable consequence of the data coming in piecemeal and asynchronously. It's inefficient at run-time, but that doesn't matter for a DPI change. But it's confusing when you're trying to figure out what's going on, and I think that DOES matter.

In my version, when the DPI changes I recreate the swap chain (etc) one time even though that's redundant because its size doesn't change. The bits that matter are letting D2D know, and changing that swap chain downscale. I'm ok with that.

-Steve