Microsoft-Build-2016 / CodeLabs-GameDev-4-GraphicsDiag

Graphics Diagnostics and Performance Tuning for Games with VS2015
MIT License
6 stars 5 forks source link



Graphics Diagnostics and Performance Tuning for Games with VS2015


Overview

There are many things that can go wrong when developing games, including memory issues, rendering issues, strange behaviors, performance issues, hardware issues and much more. In this session, you will get hands on experience with the Visual Studio Graphics Diagnostics tools to help you in situations where your game needs analysis and debugging. You will learn how the tools work, and how to use them to solve different graphical issues.

Objectives

In this module, you will:

Prerequisites

The following is required to complete this module:


Exercises

This module includes the following exercises:

  1. Profiling
  2. Debugging Shaders and Graphics
  3. Debugging Events and Pipeline Stages

Estimated time to complete this module: 60 minutes

Exercise 1: Profiling

The Graphics Diagnostics tools in Visual Studio are designed to help you locate rendering problems by starting with the visual artifacts that indicate the problem and then tracing back to the source of the problem by focusing only on relevant shader code, pipeline stages, draw calls, resources, and device state in the app's own source code.

In this exercise, you will use the diagnostic tools to profile the game to check the CPU usage and identify performance issues.

Task 1 - Playing with the game

In this task, you will play with the game to learn how it works and get familiar with the UI.

  1. Open the project file Source\Ex1\Begin\Apollo\Assets\Main.unity in UNITY and play the game a few times by clicking on the PLAY button. You control the rocket by pressing A and D on the keyboard, and launch by pressing SPACE or click the Launch button on the user interface. Above the rocket, there is a path indicator:

    Path indicator

    Path indicator

  2. Your job is to keep the red dot as centered as possible (so the offset reading below the marker stays as close to 0.00 as possible). There is an in game line that also will show where the path goes:

    Path line

    Path line

  3. The launch conditions are windy, and the wind will try to push you off the path. Use A and D to slowly rotate the rocket back on track.

    One round lasts for about 30 seconds, and you will be measured by how close you are to the path when you cross the finish line.

    Game finish

    Game finish

Task 2 - Exporting the game as a UWP

Now that we know how the game works, we want to run it through a performance analysis to see if we are having any performance issues.

  1. In Unity, click File->Build Settings

    File menu

    File menu

  2. Select Windows Store, and from the dropdown, select Universal 10.

    Build Settings

    Build Settings

  3. Make sure you also tick the Unity C# Projects and the Development Build check-boxes.

    Windows Store build settings

    Windows Store build settings

  4. Next, click Build and create a subfolder in the root project named EXPORT, and another one inside the new EXPORT folder called UWP and click Select Folder to start the export (Ex1\Begin\Apollo\EXPORT\UWP).

    Selecting build folder

    Selecting build folder

Task 3 - Opening the Visual Studio solution

  1. Once the export is done, the new folder should automatically open. Open Apollo.sln.

    Exported folder

    Exported folder

  2. Once Visual Studio loads the project, change the build configuration to x86, and make sure that it is in Debug. Click the Local Machine button to build, deploy and run the solution and test that it works:

    Build and run

    Build and run

    Note: Some build error messages might appear since this is the first time we are building the solution. This is normal as long as the build process is continuing.

  3. The game is a bit slow and laggy. A partial reason is that we are running under the debugger on a Debug build, but let's see what happens behind the scenes.

  4. Stop the game.

Task 4 - Running the Profiler

Use the GPU Usage tool in the Visual Studio Performance and Diagnostics Hub to better understand the high-level hardware utilization of your Direct3D app. You can use it to determine whether the performance of your app is CPU-bound or GPU-bound and gain insight into how you can use the platform's hardware more effectively. GPU Usage supports apps that use Direct3D 12, Direct3D 11, and Direct3D 10; it doesn't support other graphics APIs such as Direct2D or OpenGL.

  1. Click Debug->Start Diagnostics Tools Without Debugging...

    Start Diagnostics Tools Without Debugging

    Start Diagnostics Tools Without Debugging

  2. Run the game with only the CPU Usage enabled. If the GPU profiler is enabled, tick that off since we will only be using the CPU Profiler.

    GPU Usage only

    GPU Usage only

  3. Next, click Start to run the game with the profiler:

    Start button

    Start button

  4. Do a launch and let the game run for a few seconds before clicking the STOP button.

    Running and stop button

    Running and stop button

    The Graphics Diagnostics tools will gather some information.

Task 5 - Checking the CPU Profiler

  1. Go to the CPU Usage tab below the graphs.

    CPU Usage

    CPU Usage

  2. Check the CPU usage and notice that the RocketControllers::Update function is using a lot of CPU usage, in this case 48.03%.

  3. Open the RocketController::$Invoke16 tree to dive a bit deeper. If you cant see this right away or if it looks a bit different, you can do a search on it.

    Functions CPU usage

    Functions CPU usage

  4. Going into it, we can see that it's external code that's spending most of it, and that external code might not have any debugging information. Also, notice the WindRandomnesGenerator function call here. It's quite low on the CPU Usage, but we know it's calling external code so this might be the source of error.

  5. Let's do some more digging by clicking Create detailed report:

    Create detailed report

    Create detailed report

  6. This will give some more details around what we are doing, and you will see a similar screen to this:

    Detailed report

    Detailed report

    There is a lot of details on this screen, but for now, notice the Hot Path tree, and a call to Mathf.PerlinNoise. In this wind generator, we're using multiple calls to the PerlinNoise function to generate a pseudo-random wind pattern using various octaves of perlin noise added together.

Task 6 - Checking the function

  1. Going back to Unity, open the script for our RocketController:

    RocketController script

    RocketController script

    By looking at the various functions in this short script, can see that most of the functions in the Update loops are pretty straight forward except for the WindRandomnessGenerator() function we identified above.

  2. To ensure we are looking at the right performance issue, let's first try to comment out the line of code that is generating the wind. Go to the Update() function in the RocketController script, and comment the first line of code:

    Commenting out WindRandomnessGenerator call

    Commenting out WindRandomnessGenerator call

  3. Now, save the change and do a rebuild all back in the Visual Studio solution. This will work since we exported the solution with the C# projects:

    Rebuild Solution

    Rebuild Solution

  4. Run the game under CPU profiler like we did earlier:

    Start Diagnostics Tools Without Debugging

    Start Diagnostics Tools Without Debugging

  5. Go the CPU Usage tab and notice the changed values. The Update function is now at normal levels.

    Improved CPU usage

    Improved CPU usage

Task 7 - Fixing it

  1. Uncomment the call to our wind generator:

    Wind generator call

    Wind generator call

    It's calculating way too many octaves on the Perlin Noise function, one per increase in view while in our intention, we just wanted to have a few octaves with a good range between each octave.

  2. This must be changed. Change the jump in octaves to 20000, significantly reducing the number of calls. The code should look like this:

    float WindRandomnessGenerator()
    {
         float windRandomness = 0.0f;
         int octaves = 0;
         int lastOctaveDistance = 100000;
         int increaseDistancePerOctave = 20000;
    
         for (int i = 0; i < lastOctaveDistance; i += increaseDistancePerOctave)
         {
              octaves++;
              windRandomness += Mathf.PerlinNoise(
                    transform.position.x / (lastOctaveDistance - i) + 1.0f,
                    transform.position.y / (lastOctaveDistance - i) + 1.0f);
         }
    
         return windRandomness / octaves;
    }
  3. Build the game again, and run it under the CPU profile like in Task 6.

The issue is now fixed, and we now got a better framerate! However, we have more graphics issues to fix so let's move on to the next exercise!

Exercise 2: Debugging Shaders and Graphics

Making a mistake in app code is almost inevitable, whether the code is C++ or High Level Shader Language (HLSL). However, debugging HLSL code has traditionally been more difficult because it hasn’t had the rich debugging support that C++ and other languages enjoy. Graphics Diagnostics brings traditional code debugging tools to HLSL so that you can step through code, set breakpoints, and examine the contents of variables, parameters, and constant buffers.

In this exercise, you will debug the game to troubleshot an issue with the game graphics by making use of the Graphics Diagnostics tools in Visual Studio.

Task 1 - Identifying the issue

  1. Open the Begin project in Unity and play another round of the game. Focus on the moon, and notice that it looks a bit weird.

    The VS Graphics Diagnostics tools comes with a set of tools that enables you to see all the drawcalls as well as the history of selected pixels, as well as shader debugging and shader editing.

Task 2 - Exporting as a UWP

  1. Let's go ahead and build the solution. In Unity, click File->Build Settings.

    File menu

    File menu

  2. Ensure that Windows Store, and from the dropdown, Universal 10 is selected.

    Build settings

    Build settings

  3. Also, make sure the Unity C# Projects and the Development Build checkboxes is checked.

    Windows Store build settings

    Windows Store build settings

  4. Click Build and navigate into EXPORT\UWP and click Select Folder to start the export.

    Selecting build folder

    Selecting build folder

Task 3 - Opening the Visual Studio solution

  1. Once the export is done, the new folder should automatically open. Open the Apollo.sln.

    Exported folder

    Exported folder

  2. Once Visual Studio loads the project, change the build configuration to x86, and make sure that it is in Debug. Click the Local Machine button to build, deploy and run the solution and test that it works:

    Build and run

    Build and run

    Note: Some build error messages might appear since this is the first time we are building the solution. This is normal as long as the build process is continuing.

  3. Feel free to try the game and ensure that the moon is looking strange.

  4. Stop the game.

Task 4 - Running in Graphics Debug mode

  1. Let's run this with the Graphics Diagnostics by clicking Debug->Graphics->Start Diagnostics.

    Start Diagnostics

    Start Diagnostics

  2. When running the game now, you are presented with performance graphs, as well as a Capture Frame button:

    Running with diagnostics

    Running with diagnostics

  3. To capture a frame, you can click the Capture Frame button, or press Print screen on the keyboard when having the app in focus.

    We are interested in finding out what's happening with the moon. When the game starts, it's stationary on the Launchpad, and the broken white moon is visible in the background.

  4. When the moon is visible, capture a frame by clicking the button or using Print screen.

  5. Then, when the frame is captured, you can see it visible where the capture frame button was.

  6. Stop the session by clicking the Stop button:

    Stop button

    Stop button

  7. Once the app stops, you can click the captured frame by clicking on the frame header:

    Captured frame

    Captured frame

  8. A new tab will open in visual studio, containing a snapshot of the system, processes, state of the app at that time the frame was captured.

    Captured system snapshot

    Captured system snapshot

Task 5 - Navigating around in the captured frame

  1. On the left side, there is a long list of events like draw calls, and in the center we are having a picture that shows the state of the frame at a selected event.

Task 6 - Finding the moon

  1. We are interested in finding the draw call that draws our moon. Click the last event before the Present event to be sure that we are having a preview that has the moon visible.

    Last event before present

    Last event before present

  2. Now, on the frame view, click a pixel inside of the moon. A red marker will show what pixel you have selected:

    Selected moon's pixel

    Selected moon's pixel

  3. One the right side of this, in the properties view, you can see all the events that are related to this pixel:

    Pixel related events

    Pixel related events

  4. The first one is the one that clears the screen to blue, the next draws the moon and the last draws the skybox. You can click each of these to see how it looks in the frame preview.

  5. Open the event by clicking on the arrow of the event in the pixel history, and open the Triangle header. This will display information about how the pixel is affected, what shaders are being used and what color the various stages produce.

    Pixel info

    Pixel info

Task 7 - Checking the graphics pipeline

  1. Below the preview, all the way to the bottom of the screen, there are a few tabs where one is gamed Pipeline Stages. Click this:

    Pipeline Stages

    Pipeline Stages

    This will show how all the pipeline stages the selected event is having. This one is having one Input stage where all the vertices that are being used is passed into the shader stages. The next is a vertex shader, that is executed once per vertex from the input stage, then there is a pixel shader that gives a color to every pixel, and then finally an output merger that marks the end of the graphics pipeline.

  2. If you click on one of the stages, like the Input Assembler, you can see how the model currently in process looks like in the built in Visual Studio Model Viewer. You can see that this is a sphere, from what the moon is built up from.

    Moon mesh preview

    Moon mesh preview

    By the looks of it, the sphere looks right so the issue will probably be in the Pixel Shader where we give it color.

Task 8 - Debugging shader

Now, below each of the stages in the Pipline Stage view, or in the Pixel History, you can see what shader is being used, as well as a green play button. By clicking on the shader name, you can see the code of the shader. This shader will create a rim around the moon, but by the looks of it, the rim color seems to be in the center, inverse.

  1. We are interested in debugging it, so click the green play button next to the _Pixel Shade_r (the one underlined below):

    Debug pixel shader

    Debug pixel shader

  2. The shader code will become visible, with the possibility to step through the shader:

    Debugging shader code

    Debugging shader code

  3. Set a breakpoint at the last line in the shader and step through until you reach this line. Look at the content of the variable named rim.

    Shader breakpoint

    Shader breakpoint

  4. You can use the tools above the tab to step through the shader:

    Step through tools

    Step through tools

    Looking at the calculations of the rim value, we have forgotten to invert it by subtracting it from one. Let's try this, so go ahead and stop the debugger and open the shader for editing.

Task 9 - Editing the shader

  1. Click on the shader tab and edit the code so it looks like this:

    Fixing shader

    Fixing shader

    The only change is to write 1.0f - in front of saturate at the line that calculates the rim effect.

  2. Save it, and the Frame Capture will update itself to use this shader. Click the frame preview tab, and notice that the moon is now fixed!

    Fixed moon

    Fixed moon

Task 10 - Fixing issue in Unity and re-exporting

  1. Close the debugging session and go back to Unity, and find the Velvet shader we are using by navigating to the Shaders/Velvet folder:

    Velvet shader

    Velvet shader

  2. Open this in Visual Studio and make the same change to it, add 1.0f - in front of saturate.

    void surf (Input IN, inout SurfaceOutput o) {
    
         o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    
         o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
    
         half rim = 1.0f - saturate(dot (normalize(IN.viewDir), o.Normal));
    
         o.Emission = _RimColor.rgb * pow (rim, _RimPower);
    
    }
  3. Save the change.

  4. Play the game from the Unity Editor, and see that it now works well, and the moon is fixed!

Exercise 3: Debugging Events and Pipeline Stages

On of the Graphics Diagnostics tools in Visual Studio is the Graphics Pipeline Stages window, where you can investigate how the currently selected event is processed by each stage of the graphics pipeline so that you can identify where the rendering problem first appears. Examining the pipeline stages is particularly helpful when an object doesn't appear because of an incorrect transformation, or when one of the stages produces output that doesn't match what the next stage expects.

In this exercise, you will use the Pipeline Stages window to understand the events that draws the tower and detect potential performance issues.

Task 1 - Exporting the UWP

By this stage, we are already familiar with how the game works and how to export the game from Unity and attach the Graphics Diagnostic Tools. The aim here is to learn how the Event List works, and how you can use it.

  1. Export the game from Unity. In Unity, click File->Build Settings:

    File menu

    File menu

  2. Ensure that Windows Store, and from the dropdown, Universal 10 is selected. Also, make sure the Unity C# Projects and the Development Build checkboxes is checked.

    Windows Store build settings

    Windows Store build settings

  3. Click Build and navigate into EXPORT\UWP and click Select Folder to start the export.

    Selecting build folder

    Selecting build folder

Task 2 - Opening the Visual Studio solution

  1. Once the export is done, the new folder should automatically open. Open the Apollo.sln.

    Exported folder

    Exported folder

  2. Once Visual Studio loads the project, change the build configuration to x86, and make sure that it is in Debug. Click the Local Machine button to build, deploy and run the solution and test that it works:

    Build and run

    Build and run

Task 3 - Running in Graphics Debug mode

  1. Let's run this with the Graphics Diagnostics by clicking Debug->Graphics->Start Diagnostics.

    Start Diagnostics

    Start Diagnostics

  2. When running the game now, you are presented with performance graphs, as well as a Capture Frame button:

    Running with diagnostics

    Running with diagnostics

  3. To capture a frame, you can click the Capture Frame button, or press Print screen on the keyboard when having the app in focus.

  4. Stop the session by clicking the Stop button:

    Stop button

    Stop button

  5. Once the app stops, you can click the captured frame by clicking on the frame header:

    Captured frame

    Captured frame

  6. A new tab will open in visual studio, containing a snapshot of the system, processes, state of the app at that time the frame was captured.

    Captured system snapshot

    Captured system snapshot

  7. On the left side, you can see a long list of events. You can click these events to see how the frame looks at that event.

Task 4 - Finding the right clear event

  1. Let's try this. Scroll up to a bit over the middle of the list view, and you will see an event that clears the screen to a given color:

    Clear screen event

    Clear screen event

  2. Clicking this will show how the frame looks to the right of the list (in the preview):

    Frame render

    Frame render

    If you follow the events down, clicking the one by one, you will see that events draw what part of the frame like the terrain, the launchpad, the rocket, the particles, the UI and so on.

    If you do this one by one (don't do this), you can see the frame being built up slowly, piece by piece.

Task 5 - Finding the draw calls that belong to our tower

  1. By clicking through the event view, you will first see that a lot of them belong to the terrain, and then another bunch that belong to the tower.

  2. Another way of finding where about the tower draw events are, is by using the pixel history (as we used in Exercise 2)

  3. In the frame view, click on a pixel on the Launchpad tower to view the pixel history:

    Launchpad tower's pixel

    Launchpad tower's pixel

  4. You can see that there are many events that are affecting the pixel:

    Events for lunchpad tower's pixel

    Events for lunchpad tower's pixel

  5. Select one by one, starting from the top to find one that belongs to the tower:

    Selecting event

    Selecting event

  6. In this case, there are many that belong to the tower. You can identify this by seeing that a part of the tower has been drawn on the frame view, and if you select the Pipeline Stages tab, you can see the part that is currently being drawn:

    Pipeline Stages tab

    Pipeline Stages tab

Task 6 - Finding all the draw calls for our tower

  1. Once you have found one part that belongs to the tower, move upwards to you find the first drawcall for the tower, and move down until the tower has been completely drawn.

    Tower draw

    Tower draw

    The last part is found here:

    Last tower draw event

    Last tower draw event

    As you can see, there is a lot of draw calls for making the tower. Sometimes this is ok and necessary, but in this case, we are drawing parts of the model multiple times, with the same shader and graphics pipeline stage.

Task 7 - Reducing the draw calls of our tower

  1. The designer of the launchtower model created another version that got the tower parts combined into one mesh. This will reduce the drawcall we had for each floor of the tower and so on.

  2. In the End folder, the new model has replaced the existing one.

Task 8 - Testing

  1. You can directly open the exported UWP of the End project, run it and capture a frame.

  2. You can now see that the most of the tower is rendered in only a couple of drawcalls.

Summary

Thank you for going through this module. We hope that by going through these steps, you have learned how you can use the Diagnostics Tools in your own graphical projects. There are many other problems in this solution, so if you want to go even further, feel free to spend some more time with it.