Supporting screen orientation (DirectX and C++)
[ This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation ]
Many Windows 8 devices support multiple screen orientations. Your Windows Store apps using DirectX with C++ can support this feature as well when you handle the CoreWindow::SizeChanged event. Here, we'll discuss best practices for handling screen rotation in your Windows Store app using DirectX so that theWindows 8 device's graphics hardware are used efficiently and effectively.
Note These practices are fully demonstrated in the Direct2D App project template in Microsoft Visual Studio, and in the DXGI swap chain rotation sample.
Before you start, remember that graphics hardware always outputs pixel data in the same way, regardless of the orientation of the device. Windows 8 devices can determine their current display orientation (with some sort of sensor, or with a software toggle) and allow users to change the display settings. Because of this, Windows 8 itself handles the rotation of the images to ensure they are "upright" based on the orientation of the device. By default, your app receives the notification that something has changed in orientation, for example, a window size. When this happens, Windows 8 immediately rotates the image for final display. For three of the four specific screen orientations (discussed later), Windows 8 uses additional graphic resources and computation to display the final image.
For Windows Store app using DirectX, the DisplayProperties object provides basic display orientation data that your app can query. The default orientation is landscape, where the pixel width of the display is greater than the height; the alternative orientation is portrait, where the display is rotated 90 degrees in either direction and the width becomes less than the height.
Windows 8 defines four specific display orientation modes:
- Landscape—the default display orientation for Windows 8, and is considered the base or identity angle for rotation (0 degrees).
- Portrait—the display has been rotated clockwise 270 degrees (or counter-clockwise 90 degrees).
- Landscape, flipped—the display has been rotated 180 degrees (turned upside-down).
- Portrait, flipped—the display has been rotated clockwise 90 degrees (or counter-clockwise 270 degrees).
When the display rotates from one orientation to another, Windows 8 internally performs a rotation operation to align the drawn image with the new orientation, and the user sees an upright image on the screen.
Also, Windows 8 displays automatic transition animations to create a smooth user experience when shifting from one orientation to another. As the display orientation shifts, the user sees these shifts as a fixed zoom and rotation animation of the displayed screen image. Time is allocated by Windows 8 to the app for layout in the new orientation. However, for Windows Store apps using DirectX, the amount of time required for the new layout is usually less than the time allowed, because other frameworks might need more of the allocated time. Because your app will most likely never use this full time allotment, you can directly notify Windows 8 that your app has completed all the required work to handle the new display orientation, and that your app has calledIDXGISwapChain::Present to display the new image. You specify your app's early layout completion using CoreWindowResizeManager::NotifyLayoutCompleted.
Overall, this is the general process for handling changes in screen orientation:
- Use a combination of the window bounds values and the display orientation data to keep the swap chain aligned with the native display orientation of the device.
- Notify Windows 8 of the orientation of the swap chain using IDXGISwapChain1::SetRotation.
- Change the rendering code to generate images aligned with the user orientation of the device.
- Notify Windows 8 that your app is ready to continue in the new orientation by using CoreWindowResizeManager::NotifyLayoutCompleted.
To see complete code for this process as applied to both Direct2D and Direct3D rendering, download the DXGI swap chain rotation sample.
There are two methods you can take in your Windows Store app using DirectX to handle the change in orientation:
- Resize the swap chain to the new window size with IDXGISwapChain1::ResizeBuffers. This is the simplest method.
- Pre-rotate the contents of the swap chain by rendering your layout to the swap chain in a rotated orientation, and then informing Windows 8 of this "pre-rotation" by calling IDXGISwapChain1::SetRotation using the transformation matrix you used when rendering the layout. This is a more optimized method over the first one.
Let's take a look at these methods.
Method 1: Resizing the swap chain
The easiest approach to handling rotation is to let Windows 8 do it for you. Of course, because your display doesn't have a 1:1 aspect ratio, the results will look stretched or squished. The solution to this? Resize the swap chain with the height and width values for the window bounds swapped and present the contents.
To perform a basic display resize in your Windows Store app using DirectX, implement these steps:
- Handle the CoreWindow::SizeChanged event.
- Resize the swap chain to the new dimensions of the window.
- Recreate any window size dependent resources, such as your render targets and other pixel data buffers.
Now's let's look at those steps in a bit more detail.
Your first step is to register a handler for the CoreWindow::SizeChanged event. This event is raised on your app's CoreWindow every time the screen size changes, such as when the display is rotated. If you've indicated that your app supports screen rotation in the app's package.appxmanifest file, you must handle the SizeChanged event. (Otherwise, the app's display is resized automatically when the orientation changes and the swap chain contents are stretched or squished to fit the new area.)
To handle the SizeChanged event, you connect your handler for CoreWindow::SizeChanged in the required SetWindow method, which is one of the methods of the IFrameworkView interface that your view provider must implement.
In this code example, the event handler for CoreWindow::SizeChanged is a method called OnWindowSizeChanged and it's also defined on the view provider object. When CoreWindow::SizeChanged is raised, it in turn calls a method called UpdateForWindowSizeChange defined on the renderer object.
void MyDirectXApp::SetWindow(
_In_ CoreWindow^ window
)
{
// ... Other UI event handlers assigned here ...
window->SizeChanged += ref new TypedEventHandler<CoreWindow^, WindowSizeChangedEventArgs^>(
this,
&MyDirectXApp::OnWindowSizeChanged
);
// ...
}
void MyDirectXApp::OnWindowSizeChanged(CoreWindow^ sender, WindowSizeChangedEventArgs^ args)
{
m_renderer->UpdateForWindowSizeChange(); // m_renderer is an object that inherits from DirectXBase
}
Now, resize the swap chain from your callback. (In the code example this is performed in DirectXBase::UpdateForWindowSizeChange.)
// This routine is called in the event handler for the view SizeChanged event.
void DirectXBase::UpdateForWindowSizeChange()
{
if (m_window->Bounds.Width != m_windowBounds.Width ||
m_window->Bounds.Height != m_windowBounds.Height ||
m_orientation != DisplayProperties::CurrentOrientation)
{
m_d2dContext->SetTarget(nullptr);
m_d2dTargetBitmap = nullptr;
m_d3dRenderTargetView = nullptr;
m_d3dDepthStencilView = nullptr;
m_windowSizeChangeInProgress = true;
CreateWindowSizeDependentResources();
}
}
// Allocate all memory resources that change on a window SizeChanged event.
void DirectXBase::CreateWindowSizeDependentResources()
{
// Store the window bounds so the next time we get a SizeChanged event we can
// avoid rebuilding everything if the size is identical.
m_windowBounds = m_window->Bounds;
if (m_swapChain != nullptr)
{
// If the swap chain already exists, resize it.
HRESULT hr = m_swapChain->ResizeBuffers(
2,
static_cast<UINT>(m_renderTargetSize.Width),
static_cast<UINT>(m_renderTargetSize.Height),
DXGI_FORMAT_B8G8R8A8_UNORM,
0
);
if (hr == DXGI_ERROR_DEVICE_REMOVED)
{
// If the device was removed for any reason, a new device and swapchain will need to be created.
HandleDeviceLost();
// Everything is set up now. Don't continue to execute this method.
return;
}
else
{
DX::ThrowIfFailed(hr);
}
}
else
{
// ...
// Otherwise, create a new one using the same adapter as the existing Direct3D device.
// ...
}
// Create a Direct3D render target view of the swap chain back buffer.
ComPtr<ID3D11Texture2D> backBuffer;
DX::ThrowIfFailed(
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))
);
DX::ThrowIfFailed(
m_d3dDevice->CreateRenderTargetView(
backBuffer.Get(),
nullptr,
&m_d3dRenderTargetView
)
);
// ...
// Set up other window size dependent resources like Direct2D contexts, depth stencils,
// and 3D viewports here...
// ...
}
In this method, you also reconfigure or resize any other window size-dependent DirectX resources, like your depth stencils or other pixel data buffers, and the Direct3D viewports if you have a 3-D scene to display.
Now, with the swap chain effectively resized and presented, the app's screen contents are correctly displayed in the new orientation after Windows 8 completes the system rotation animation. However, Windows 8 still performs its own pixel data copy of the full screen for the rotation animation, taking a snapshot of the screen buffer and rotating the image itself before displaying your app's presented swap chain. You can eliminate this redundancy and save the GPU some work (especially if the native resolution is high) by telling Windows 8 that you already rotated the rendering pipeline. We'll look at this potential optimization in the next section. We also look at handling Direct2D and Direct3D components of the swap chain, both of which require different techniques when compositing the rotated image.
For complete code that shows how this process works, review the Direct2D App template in Visual Studio, or download the DXGI swap chain rotation sample.
Method 2: Pre-rotate the contents of the swap chain
Direct3D 11 and DXGI provide a new API,IDXGISwapChain1::SetRotation, that you can use in your Windows Store app using DirectX to notify Windows 8 of the orientation of the window image data in the swap chain. If the orientation of the device and the orientation of the swap chain match, Windows 8 can use the window images directly without performing any rotation, and you avoid an additional full screen copy operation at the hardware level, returning performance and battery life savings back to the app. However, your app must render the images into the swap chain to match the orientation so that you can pick up this additional efficiency.
As we've discussed previously, at the minimum, your Windows Store app using DirectX can simply handle a rotation event by resizing the swap chain's buffers to the new height and width of the screen and then presenting it.
The process gets the job done, but there are a couple negatives to it:
- It performs a redundant full screen copy operation.
- It really doesn't differentiate between the type of size change event, which could have been fired by an app window moving to a snapped or filled state, or by a change in display resolution.
Let's update the process to address these inefficiencies by following these updated steps:
- Handle the CoreWindow::SizeChanged event, as before.
- Use DisplayProperties::CurrentOrientation to confirm that the window size change event is specifically for a rotation.
- Don’t resize the swap chain! Instead, use the current orientation data to update the Direct2D and Direct3D transformations held by the app and rotate the rendering pipeline.
- Call IDXGISwapChain1::SetRotation to set the orientation of the swap chain.
- Recreate any window size dependent resources, as before.
Here's the long form work for resizing the swap chain for the new screen orientation and preparing it to rotate the contents of the graphic pipeline when the rendering is performed. In this example, DirectXBase::CreateWindowSizeDependentResources is a method you'd implement on your renderer object.
Take a moment to review the code, and then we'll discuss it after the jump..
// Allocate all memory resources that change on a window SizeChanged event.
void DirectXBase::CreateWindowSizeDependentResources()
{
// Store the window bounds so the next time we get a SizeChanged event we can
// avoid rebuilding everything if the size is identical.
m_windowBounds = m_window->Bounds;
// Calculate the necessary swap chain and render target size in pixels.
auto windowWidth = ConvertDipsToPixels(m_windowBounds.Width);
auto windowHeight = ConvertDipsToPixels(m_windowBounds.Height);
// Swap width and height based on orientation.
m_orientation = DisplayProperties::CurrentOrientation;
bool swapDimensions = (
m_orientation == DisplayOrientations::Portrait ||
m_orientation == DisplayOrientations::PortraitFlipped
);
m_renderTargetSize.Width = swapDimensions ? windowHeight : windowWidth;
m_renderTargetSize.Height = swapDimensions ? windowWidth : windowHeight;
if (m_swapChain != nullptr)
{
// If the swap chain already exists, resize it.
HRESULT hr = m_swapChain->ResizeBuffers(
2,
static_cast<UINT>(m_renderTargetSize.Width),
static_cast<UINT>(m_renderTargetSize.Height),
DXGI_FORMAT_B8G8R8A8_UNORM,
0
);
if (hr == DXGI_ERROR_DEVICE_REMOVED)
{
// If the device was removed for any reason, a new device and swapchain will need to be created.
HandleDeviceLost();
// Everything is set up now. Don't continue to execute this method.
return;
}
else
{
DX::ThrowIfFailed(hr);
}
}
else
{
// Otherwise, create a new one using the same adapter as the existing Direct3D device.
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {0};
swapChainDesc.Width = static_cast<UINT>(m_renderTargetSize.Width); // Match the size of the window.
swapChainDesc.Height = static_cast<UINT>(m_renderTargetSize.Height);
swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // This is the most common swap chain format.
swapChainDesc.Stereo = false;
swapChainDesc.SampleDesc.Count = 1; // Don't use multi-sampling.
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 2; // Use double-buffering to minimize latency.
swapChainDesc.Scaling = DXGI_SCALING_NONE;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // All Windows Store apps must use this SwapEffect.
swapChainDesc.Flags = 0;
ComPtr<IDXGIDevice1> dxgiDevice;
DX::ThrowIfFailed(
m_d3dDevice.As(&dxgiDevice)
);
ComPtr<IDXGIAdapter> dxgiAdapter;
DX::ThrowIfFailed(
dxgiDevice->GetAdapter(&dxgiAdapter)
);
ComPtr<IDXGIFactory2> dxgiFactory;
DX::ThrowIfFailed(
dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory))
);
CoreWindow^ window = m_window.Get();
DX::ThrowIfFailed(
dxgiFactory->CreateSwapChainForCoreWindow(
m_d3dDevice.Get(),
reinterpret_cast<IUnknown*>(window),
&swapChainDesc,
nullptr,
&m_swapChain
)
);
// Ensure that DXGI doesn't queue more than one frame at a time. This both reduces latency and
// ensures that the application will only render after each VSync, minimizing power consumption.
DX::ThrowIfFailed(
dxgiDevice->SetMaximumFrameLatency(1)
);
}
// Set the proper orientation for the swap chain, and generate 2-D and
// 3-D matrix transformations for rendering to the rotated swap chain.
// Note the rotation angle for the 2-D and 3-D transforms are different.
// This is due to the difference in coordinate spaces. Additionally,
// the 3-D matrix is specified explicitly to avoid rounding errors.
DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
switch (m_orientation)
{
case DisplayOrientations::Landscape:
rotation = DXGI_MODE_ROTATION_IDENTITY;
m_rotationTransform2D = Matrix3x2F::Identity();
m_rotationTransform3D = identity();
break;
case DisplayOrientations::Portrait:
rotation = DXGI_MODE_ROTATION_ROTATE270;
m_rotationTransform2D =
Matrix3x2F::Rotation(270.0f) *
Matrix3x2F::Translation(0.0f, m_windowBounds.Width);
m_rotationTransform3D = float4x4( // 90-degree Z-rotation
0.0f, -1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
case DisplayOrientations::LandscapeFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE180;
m_rotationTransform2D =
Matrix3x2F::Rotation(180.0f) *
Matrix3x2F::Translation(m_windowBounds.Width, m_windowBounds.Height);
m_rotationTransform3D = float4x4( // 180-degree Z-rotation
-1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
case DisplayOrientations::PortraitFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE90;
m_rotationTransform2D =
Matrix3x2F::Rotation(90.0f) *
Matrix3x2F::Translation(m_windowBounds.Height, 0.0f);
m_rotationTransform3D = float4x4( // 270-degree Z-rotation
0.0f, 1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
default:
throw ref new Platform::FailureException();
break;
}
DX::ThrowIfFailed(
m_swapChain->SetRotation(rotation)
);
// Create a Direct3D render target view of the swap chain back buffer.
ComPtr<ID3D11Texture2D> backBuffer;
DX::ThrowIfFailed(
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))
);
DX::ThrowIfFailed(
m_d3dDevice->CreateRenderTargetView(
backBuffer.Get(),
nullptr,
&m_d3dRenderTargetView
)
);
// Create a depth stencil view for use with 3-D rendering if needed.
CD3D11_TEXTURE2D_DESC depthStencilDesc(
DXGI_FORMAT_D24_UNORM_S8_UINT,
static_cast<UINT>(m_renderTargetSize.Width),
static_cast<UINT>(m_renderTargetSize.Height),
1,
1,
D3D11_BIND_DEPTH_STENCIL
);
ComPtr<ID3D11Texture2D> depthStencil;
DX::ThrowIfFailed(
m_d3dDevice->CreateTexture2D(
&depthStencilDesc,
nullptr,
&depthStencil
)
);
auto viewDesc = CD3D11_DEPTH_STENCIL_VIEW_DESC(D3D11_DSV_DIMENSION_TEXTURE2D);
DX::ThrowIfFailed(
m_d3dDevice->CreateDepthStencilView(
depthStencil.Get(),
&viewDesc,
&m_d3dDepthStencilView
)
);
// Set the 3-D rendering viewport to target the entire window.
CD3D11_VIEWPORT viewport(
0.0f,
0.0f,
m_renderTargetSize.Width,
m_renderTargetSize.Height
);
m_d3dContext->RSSetViewports(1, &viewport);
// Create a Direct2D target bitmap associated with the
// swap chain back buffer and set it as the current target.
D2D1_BITMAP_PROPERTIES1 bitmapProperties =
BitmapProperties1(
D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
m_dpi,
m_dpi
);
ComPtr<IDXGISurface> dxgiBackBuffer;
DX::ThrowIfFailed(
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer))
);
DX::ThrowIfFailed(
m_d2dContext->CreateBitmapFromDxgiSurface(
dxgiBackBuffer.Get(),
&bitmapProperties,
&m_d2dTargetBitmap
)
);
m_d2dContext->SetTarget(m_d2dTargetBitmap.Get());
// Grayscale text anti-aliasing is recommended for all Windows Store apps.
m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
}
After saving the current height and width values of the window for the next time this method is called, convert the device independent pixel (DIP) values for the display bounds to pixels. In the sample, you call ConvertDipsToPixels, which is a simple function that runs this code:
floor((dips * DisplayProperties::LogicalDpi / 96.0f) + 0.5f);
You add the 0.5f to ensure rounding to the nearest integer value.
As an aside, CoreWindow coordinates are always defined in DIPs. For Windows 8 and earlier versions of Windows, a DIP is defined as 1/96th of an inch, and aligned to the OS's definition of up. When the display orientation rotates to portrait mode, the app flips the width and height of the CoreWindow, and the render target size (bounds) must change accordingly. Because Direct3D’s coordinates are always in physical pixels, you must convert from CoreWindow's DIP values to integer pixel values before you pass these values to Direct3D to set up the swap chain.
Process-wise, you're doing a bit more work than you would if you simply resized the swap chain: you're actually rotating the Direct2D and Direct3D components of your image before you composite them for presentation, and you're telling the swap chain that you've rendered the results in a new orientation. Here's a little more detail on this process, as shown in the code example for DirectXBase::CreateWindowSizeDependentResources:
Determine the new orientation of the display. If the display has flipped from landscape to portrait, or vice versa, swap the height and width values—changed from DIP values to pixels, of course—for the display bounds.
Then, check to see if the swap chain has been created. If it hasn't been created, create it by calling IDXGIFactory2::CreateSwapChainForCoreWindow. Otherwise, resize the existing swap chain's buffers to the new display dimensions by calling IDXGISwapchain:ResizeBuffers. Although you don't need to resize the swap chain for the rotation event—you're outputting the content already rotated by your rendering pipeline, after all—there are other size change events, such as snap and fill events, that require resizing.
After that, set the appropriate 2-D or 3-D matrix transformation to apply to the pixels or the vertices (respectively) in the graphics pipeline when rendering them to the swap chain. We have 4 possible rotation matrices:
- landscape (DXGI_MODE_ROTATION_IDENTITY)
- portrait (DXGI_MODE_ROTATION_ROTATE270)
- landscape, flipped (DXGI_MODE_ROTATION_ROTATE180)
- portrait, flipped (DXGI_MODE_ROTATION_ROTATE90)
The correct matrix is selected based on the data provided by Windows 8 (such as the results of DisplayProperties::OrientationChanged or SimpleOrientationSensor::OrientationChanged) for determining display orientation, and it will be multiplied by the coordinates of each pixel (Direct2D) or vertex (Direct3D) in the scene, effectively rotating them to align to the orientation of the screen. (Note that in Direct2D, the screen origin is defined as the upper-left corner, while in Direct3D the origin is defined as the logical center of the window.)
Note For more info about the 2-D transformations used for rotation and how to define them, see Defining matrices for screen rotation (2-D). For more info about the 3-D transformations used for rotation, see Defining matrices for screen rotation (3-D).
Now, here's the important bit: call IDXGISwapChain1::SetRotation and provide it with your updated rotation matrix, like this:
m_swapChain->SetRotation(rotation);
You also store the selected rotation matrix where your render method can get it when it computes the new projection. You'll use this matrix when you render your final 3-D projection or composite your final 2-D layout. (It doesn't automatically apply it for you.)
After that, create a new render target for the rotated 3-D view, as well as a new depth stencil buffer for the view. Set the 3-D rendering viewport for the rotated scene by calling ID3D11DeviceContext:RSSetViewports.
Lastly, if you have 2-D images to rotate or lay out, create a 2-D render target as a writable bitmap for the resized swap chain using ID2D1DeviceContext::CreateBitmapFromDxgiSurface and composite your new layout for the updated orientation. Set any properties you need to on the render target, such as the anti-aliasing mode (as seen in the code example).
Now, present the swap chain.
Reduce the rotation delay by using CoreWindowResizeManager
By default, Windows 8 provides a short but noticeable window of time for any app, regardless of app model or language, to complete the rotation of the image. However, chances are that when your app performs the rotation calculation using one of the techniques described here, it will be done well before this window of time has closed. You'd like to get that time back and complete the rotation animation, right? That's where CoreWindowResizeManager comes in.
Here's how to use CoreWindowResizeManager: when a CoreWindow::SizeChanged event is raised, call CoreWindowResizeManager::GetForCurrentView within the handler for the event to obtain an instance of CoreWindowResizeManager and, when the layout for the new orientation is complete and presented, call the NotifyLayoutCompleted to let Windows know that it can complete the rotation animation and display the app screen.
Here's what the code in your event handler for CoreWindow::SizeChanged might look like:
CoreWindowResizeManager^ resizeManager = Windows::UI::Core::CoreWindowResizeManager::GetForCurrentView();
// ... build the layout for the new display orientation ...
resizeManager->NotifyLayoutCompleted();
When a user rotates the orientation of the display, Windows 8 shows an animation independent of your app as feedback to the user. There are three parts to that animation that happen in the following order:
- Windows 8 shrinks the original image.
- Windows 8 holds the image for the time it takes to rebuild the new layout. This is the window of time that you'd like to reduce, because your app probably doesn't need all of it.
- When the layout window expires, or when a notification of layout completion is received, Windows rotates the image and then cross-fade zooms to new orientation.
As suggested in the third bullet, when an app calls NotifyLayoutCompleted, Windows 8 stops the timeout window, completes the rotation animation and returns control to your app, which is now drawing in the new display orientation. The overall effect is that your app now feels a little bit more fluid and responsive, and works a little more efficiently!
Appendix A: Applying matrices for screen rotation (2-D)
In the sample in Optimizing the rotation process (and in the DXGI swap chain rotation sample), you might have noticed that we had separate rotation matrices for Direct2D output and Direct3D output. Let's look at the 2-D matrices, first.
There are two reasons that we can't apply the same rotation matrices to Direct2D and Direct3D content:
One, they use different Cartesian coordinate models. Direct2D uses the right-handed rule, where the y-coordinate increases in positive value moving upward from the origin. However, Direct3D uses the left-handed rule, where the y-coordinate increases in positive value rightward from the origin. The result is the origin for the screen coordinates is located in the upper-left for Direct2D, while the origin for the screen (the projection plane) is in the lower-left for Direct3D. (See 3-D coordinate systems for more info.)
Two, the 3-D rotation matrices must be specified explicitly to avoid rounding errors.
The swap chain assumes that the origin is located in the lower-left, so you must perform a rotation to align the right-handed Direct2D coordinate system with the left-handed one used by the swap chain. Specifically, you reposition the image under the new left-handed orientation by multiplying the rotation matrix with a translation matrix for the rotated coordinate system origin, and transform the image from the CoreWindow's coordinate space to the swap chain's coordinate space. Your app also must consistently apply this transform when the Direct2D render target is connected with the swap chain. However, if your app is drawing to intermediate surfaces that are not associated directly with the swap chain, don't apply this coordinate space transformation.
Your code to select the correct matrix from the four possible rotations might look like this (be aware of the translation to the new coordinate system origin):
// Set the proper orientation for the swap chain, and generate 2-D and
// 3-D matrix transformations for rendering to the rotated swap chain.
DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
switch (m_orientation)
{
case DisplayOrientations::Landscape:
rotation = DXGI_MODE_ROTATION_IDENTITY;
m_rotationTransform2D = Matrix3x2F::Identity();
break;
case DisplayOrientations::Portrait:
rotation = DXGI_MODE_ROTATION_ROTATE270;
m_rotationTransform2D =
Matrix3x2F::Rotation(270.0f) *
Matrix3x2F::Translation(0.0f, m_windowBounds.Width);
break;
case DisplayOrientations::LandscapeFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE180;
m_rotationTransform2D =
Matrix3x2F::Rotation(180.0f) *
Matrix3x2F::Translation(m_windowBounds.Width, m_windowBounds.Height);
break;
case DisplayOrientations::PortraitFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE90;
m_rotationTransform2D =
Matrix3x2F::Rotation(90.0f) *
Matrix3x2F::Translation(m_windowBounds.Height, 0.0f);
break;
default:
throw ref new Platform::FailureException();
break;
}
After you have the correct rotation matrix and origin for the 2-D image, set it with a call to ID2D1DeviceContext::SetTransform between your calls to ID2D1DeviceContext::BeginDraw and ID2D1DeviceContext::EndDraw.
Warning Direct2D doesn't have a transformation stack. If your app is also using the ID2D1DeviceContext::SetTransform as a part of its drawing code, this matrix needs to be post-multiplied to any other transform you have applied.
m_d2dContext->BeginDraw();
// draw reference bitmap at the center of the screen
m_d2dContext->SetTransform(
Matrix3x2F::Translation(
m_windowBounds.Width / 2.0f - bitmapWidth / 2.0f,
m_windowBounds.Height / 2.0f - bitmapHeight / 2.0f
) *
m_rotationTransform2D // apply 2D prerotation transform
);
m_d2dContext->DrawBitmap(m_referenceBitmap.Get());
m_d2dcontext->EndDraw();
In the previous example, you also center the image by multiplying the new rotation matrix by yet another translation matrix that specifies the upper-left corner of the image, which must always be above and left of the screen center after rotation occurs. (This is an optional step.) Draw the rotated image to the device context by using a call to ID2D1DeviceContext::DrawBitmap.
The next time you present the swap chain, your 2-D image will be rotated to match the new display orientation.
Appendix B: Applying matrices for screen rotation (3-D)
In the sample in Optimizing the rotation process (and in the DXGI swap chain rotation sample), we defined a specific transformation matrix for each possible screen orientation. Now, let's look at the matrixes for rotating 3-D scenes. As before, you create a set of matrices for each of the 4 possible orientations. To prevent rounding errors and thus minor visual artifacts, declare the matrices explicitly in your code.
You set up these 3-D rotation matrices as follows. The matrices shown in the following code example are standard rotation matrices for 0, 90, 180, and 270 degree rotations of the vertices that define points in the camera's 3-D scene space. Each vertex's [x, y, z] coordinate value in the scene is multiplied by this rotation matrix when the 2-D projection of the scene is computed.
// Set the proper orientation for the swap chain, and generate the
// 3-D matrix transformation for rendering to the rotated swap chain.
DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
switch (m_orientation)
{
case DisplayOrientations::Landscape:
rotation = DXGI_MODE_ROTATION_IDENTITY;
m_orientationTransform3D = XMFLOAT4X4( // 0-degree Z-rotation
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
case DisplayOrientations::Portrait:
rotation = DXGI_MODE_ROTATION_ROTATE270;
m_orientationTransform3D = XMFLOAT4X4( // 90-degree Z-rotation
0.0f, 1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
case DisplayOrientations::LandscapeFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE180;
m_orientationTransform3D = XMFLOAT4X4( // 180-degree Z-rotation
-1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
case DisplayOrientations::PortraitFlipped:
rotation = DXGI_MODE_ROTATION_ROTATE90;
m_orientationTransform3D = XMFLOAT4X4( // 270-degree Z-rotation
0.0f, -1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
break;
default:
throw ref new Platform::FailureException();
}
You set the rotation type on the swap chain with a call to IDXGISwapChain1::SetRotation, like this:
m_swapChain->SetRotation(rotation);
Now, in your render method, implement some code similar to this:
struct ConstantBuffer // this struct provided for illustration
{
// other constant buffer matrices and data defined here
float4x4 projection; // current matrix for projection
} ;
ConstantBuffer m_constantBufferData; // constant buffer resource data
// ...
// rotate the projection matrix as it will be used to render to the rotated swap chain
m_constantBufferData.projection = mul(m_constantBufferData.projection, m_rotationTransform3D);
Now, when you call your render method, it multiplies the current rotation matrix (as specified by the class variable m_orientationTransform3D) with the current projection matrix, and assigns the results of that operation as the new projection matrix for your renderer. Present the swap chain to see the scene in the updated display orientation!