├── .gitignore ├── LICENSE.md ├── README.md ├── baldurk ├── custom-shader-templates │ ├── README.md │ ├── glsl-template.glsl │ └── hlsl-template.hlsl └── whereismydraw │ ├── README.md │ ├── __init__.py │ ├── analyse.py │ ├── extension.json │ └── window.py ├── docs ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md └── example └── empty-extension ├── README.md ├── __init__.py └── extension.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Baldur Karlsson and other extension authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RenderDoc community contributed extensions 2 | 3 | This repository contains UI extensions and custom display shaders that have been written by the community for [RenderDoc](https://github.com/baldurk/renderdoc). 4 | 5 | Note that by design the code here is directly contributed by the community and is **not** written by the RenderDoc authors. Ensure that you trust the extension authors or verify that the code doesn't do anything dangerous, since unless it is obvious the code is **not** vetted for security or safety. 6 | 7 | # Installing 8 | 9 | To install, you can download or clone this project under your `qrenderdoc` settings folder in an `extensions` subdirectory. For example: 10 | 11 | * Windows: `%APPDATA%\qrenderdoc\extensions\renderdoc-contrib` 12 | * Linux: `~/.local/share/qrenderdoc/extensions/renderdoc-contrib` 13 | 14 | RenderDoc will populate all compatible extensions on next startup, and you can manage these from `Tools` → `Manage Extensions`. Extensions can be loaded for a single session by clicking 'load', and loaded for all sessions by then selecting 'always load'. 15 | 16 | For more information on setting up extensions consult the [RenderDoc documentation](https://renderdoc.org/docs/how/how_python_extension.html). For writing extensions there is [specific documentation](https://renderdoc.org/docs/python_api/ui_extensions.html). 17 | 18 | To use a custom shader you can either configure a new path in the settings window under `Texture Viewer` and `Custom shader directories`, or else copy the file you want to use into your `qrenderdoc` settings folder as above (the parent folder of the `extensions` subdirectory). 19 | 20 | # Contributing 21 | 22 | Community-written extensions are welcome and this repository is intended to collate them so more people can find useful extensions. You can open a PR to add or update your extension and it will be merged here as soon as possible. 23 | 24 | For more information on contributing see [the contributing guidelines](docs/CONTRIBUTING.md) 25 | -------------------------------------------------------------------------------- /baldurk/custom-shader-templates/README.md: -------------------------------------------------------------------------------- 1 | # Custom shader templates 2 | 3 | These are simple shader templates. All they do is invert (1 - x) the sampled texture data, but they handle all APIs and texture types correctly so it can be useful as a template to build a shader from. 4 | 5 | __NOTE__: These shaders are based on the new binding abstraction macros added in v1.19. See the history on this directory for templates that worked on previous versions, though note that e.g. the HLSL template will *not* work on Vulkan. 6 | -------------------------------------------------------------------------------- /baldurk/custom-shader-templates/glsl-template.glsl: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | 3 | ///////////////////////////////////// 4 | // Constants // 5 | ///////////////////////////////////// 6 | 7 | // possible values (these are only return values from this function, NOT texture binding points): 8 | // RD_TextureType_1D 9 | // RD_TextureType_2D 10 | // RD_TextureType_3D 11 | // RD_TextureType_Cube (OpenGL only) 12 | // RD_TextureType_1D_Array (OpenGL only) 13 | // RD_TextureType_2D_Array (OpenGL only) 14 | // RD_TextureType_Cube_Array (OpenGL only) 15 | // RD_TextureType_Rect (OpenGL only) 16 | // RD_TextureType_Buffer (OpenGL only) 17 | // RD_TextureType_2DMS 18 | // RD_TextureType_2DMS_Array (OpenGL only) 19 | uint RD_TextureType(); 20 | 21 | // selected sample, or -numSamples for resolve 22 | int RD_SelectedSample(); 23 | 24 | uint RD_SelectedSliceFace(); 25 | 26 | uint RD_SelectedMip(); 27 | 28 | // xyz = width, height, depth (or array size). w = # mips 29 | uvec4 RD_TexDim(); 30 | 31 | // x = horizontal downsample rate (1 full rate, 2 half rate) 32 | // y = vertical downsample rate 33 | // z = number of planes in input texture 34 | // w = number of bits per component (8, 10, 16) 35 | uvec4 RD_YUVDownsampleRate(); 36 | 37 | // x = where Y channel comes from 38 | // y = where U channel comes from 39 | // z = where V channel comes from 40 | // w = where A channel comes from 41 | // each index will be [0,1,2,3] for xyzw in first plane, 42 | // [4,5,6,7] for xyzw in second plane texture, etc. 43 | // it will be 0xff = 255 if the channel does not exist. 44 | uvec4 RD_YUVAChannels(); 45 | 46 | // a pair with minimum and maximum selected range values 47 | vec2 RD_SelectedRange(); 48 | 49 | ///////////////////////////////////// 50 | 51 | 52 | ///////////////////////////////////// 53 | // Resources // 54 | ///////////////////////////////////// 55 | 56 | // Float Textures 57 | layout (binding = RD_FLOAT_1D_ARRAY_BINDING) uniform sampler1DArray tex1DArray; 58 | layout (binding = RD_FLOAT_2D_ARRAY_BINDING) uniform sampler2DArray tex2DArray; 59 | layout (binding = RD_FLOAT_3D_BINDING) uniform sampler3D tex3D; 60 | layout (binding = RD_FLOAT_2DMS_ARRAY_BINDING) uniform sampler2DMSArray tex2DMSArray; 61 | 62 | // YUV textures only supported on vulkan 63 | #ifdef VULKAN 64 | layout(binding = RD_FLOAT_YUV_ARRAY_BINDING) uniform sampler2DArray texYUVArray[2]; 65 | #endif 66 | 67 | // OpenGL has more texture types to match 68 | #ifndef VULKAN 69 | layout (binding = RD_FLOAT_1D_BINDING) uniform sampler1D tex1D; 70 | layout (binding = RD_FLOAT_2D_BINDING) uniform sampler2D tex2D; 71 | layout (binding = RD_FLOAT_CUBE_BINDING) uniform samplerCube texCube; 72 | layout (binding = RD_FLOAT_CUBE_ARRAY_BINDING) uniform samplerCubeArray texCubeArray; 73 | layout (binding = RD_FLOAT_RECT_BINDING) uniform sampler2DRect tex2DRect; 74 | layout (binding = RD_FLOAT_BUFFER_BINDING) uniform samplerBuffer texBuffer; 75 | layout (binding = RD_FLOAT_2DMS_BINDING) uniform sampler2DMS tex2DMS; 76 | #endif 77 | 78 | // Int Textures 79 | layout (binding = RD_INT_1D_ARRAY_BINDING) uniform isampler1DArray texSInt1DArray; 80 | layout (binding = RD_INT_2D_ARRAY_BINDING) uniform isampler2DArray texSInt2DArray; 81 | layout (binding = RD_INT_3D_BINDING) uniform isampler3D texSInt3D; 82 | layout (binding = RD_INT_2DMS_ARRAY_BINDING) uniform isampler2DMSArray texSInt2DMSArray; 83 | 84 | #ifndef VULKAN 85 | layout (binding = RD_INT_1D_BINDING) uniform isampler1D texSInt1D; 86 | layout (binding = RD_INT_2D_BINDING) uniform isampler2D texSInt2D; 87 | layout (binding = RD_INT_RECT_BINDING) uniform isampler2DRect texSInt2DRect; 88 | layout (binding = RD_INT_BUFFER_BINDING) uniform isamplerBuffer texSIntBuffer; 89 | layout (binding = RD_INT_2DMS_BINDING) uniform isampler2DMS texSInt2DMS; 90 | #endif 91 | 92 | // Unsigned int Textures 93 | layout (binding = RD_UINT_1D_ARRAY_BINDING) uniform usampler1DArray texUInt1DArray; 94 | layout (binding = RD_UINT_2D_ARRAY_BINDING) uniform usampler2DArray texUInt2DArray; 95 | layout (binding = RD_UINT_3D_BINDING) uniform usampler3D texUInt3D; 96 | layout (binding = RD_UINT_2DMS_ARRAY_BINDING) uniform usampler2DMSArray texUInt2DMSArray; 97 | 98 | #ifndef VULKAN 99 | layout (binding = RD_UINT_1D_BINDING) uniform usampler1D texUInt1D; 100 | layout (binding = RD_UINT_2D_BINDING) uniform usampler2D texUInt2D; 101 | layout (binding = RD_UINT_RECT_BINDING) uniform usampler2DRect texUInt2DRect; 102 | layout (binding = RD_UINT_BUFFER_BINDING) uniform usamplerBuffer texUIntBuffer; 103 | layout (binding = RD_UINT_2DMS_BINDING) uniform usampler2DMS texUInt2DMS; 104 | #endif 105 | 106 | ///////////////////////////////////// 107 | 108 | vec4 get_pixel_col(vec2 uv) 109 | { 110 | vec4 col = vec4(0,0,0,0); 111 | 112 | int sampleCount = -RD_SelectedSample(); 113 | 114 | uvec4 texRes = RD_TexDim(); 115 | 116 | uint mip = RD_SelectedMip(); 117 | uint sliceFace = RD_SelectedSliceFace(); 118 | 119 | // RD_TexDim() is always the dimension of the texture. When loading from smaller mips, we need to multiply 120 | // uv by the mip dimension 121 | texRes.x = max(1u, texRes.x >> mip); 122 | texRes.y = max(1u, texRes.y >> mip); 123 | 124 | // handle OpenGL-specific types, including non-arrayed versions 125 | #ifndef VULKAN 126 | if(RD_TextureType() == RD_TextureType_1D) 127 | { 128 | return texelFetch(tex1D, int(uv.x * texRes.x), int(mip)); 129 | } 130 | else if(RD_TextureType() == RD_TextureType_2D) 131 | { 132 | return texelFetch(tex2D, ivec2(uv * texRes.xy), int(mip)); 133 | } 134 | else if(RD_TextureType() == RD_TextureType_Cube) 135 | { 136 | // don't handle cubemaps here, GL needs you to generate a cubemap lookup vector 137 | } 138 | else if(RD_TextureType() == RD_TextureType_Cube_Array) 139 | { 140 | // don't handle cubemaps here, GL needs you to generate a cubemap lookup vector 141 | } 142 | else if(RD_TextureType() == RD_TextureType_Rect) 143 | { 144 | return texelFetch(tex2DRect, ivec2(uv * texRes.xy)); 145 | } 146 | else if(RD_TextureType() == RD_TextureType_Buffer) 147 | { 148 | return texelFetch(texBuffer, int(uv.x * texRes.x)); 149 | } 150 | else if(RD_TextureType() == RD_TextureType_2DMS) 151 | { 152 | if(sampleCount < 0) 153 | { 154 | // worst resolve you've seen in your life 155 | for(int i = 0; i < sampleCount; i++) 156 | col += texelFetch(tex2DMS, ivec2(uv * texRes.xy), i); 157 | 158 | col /= float(sampleCount); 159 | 160 | return col; 161 | } 162 | else 163 | { 164 | return texelFetch(tex2DMS, ivec2(uv * texRes.xy), sampleCount); 165 | } 166 | } 167 | #endif 168 | 169 | // we check for both array and non-array types here, since vulkan just 170 | // reports "1D" whereas GL will report "1D Array" 171 | if(RD_TextureType() == RD_TextureType_1D || RD_TextureType() == RD_TextureType_1D_Array) 172 | { 173 | return texelFetch(tex1DArray, ivec2(uv.x * texRes.x, sliceFace), int(mip)); 174 | } 175 | else if(RD_TextureType() == RD_TextureType_2D || RD_TextureType() == RD_TextureType_2D_Array) 176 | { 177 | return texelFetch(tex2DArray, ivec3(uv * texRes.xy, sliceFace), int(mip)); 178 | } 179 | else if(RD_TextureType() == RD_TextureType_3D) 180 | { 181 | col = texelFetch(tex3D, ivec3(uv * texRes.xy, sliceFace), int(mip)); 182 | } 183 | else if(RD_TextureType() == RD_TextureType_2DMS || RD_TextureType() == RD_TextureType_2DMS_Array) 184 | { 185 | if(sampleCount < 0) 186 | { 187 | // worst resolve you've seen in your life 188 | for(int i = 0; i < sampleCount; i++) 189 | col += texelFetch(tex2DMSArray, ivec3(uv * texRes.xy, sliceFace), i); 190 | 191 | col /= float(sampleCount); 192 | } 193 | else 194 | { 195 | col = texelFetch(tex2DMSArray, ivec3(uv * texRes.xy, sliceFace), sampleCount); 196 | } 197 | } 198 | 199 | return col; 200 | } 201 | 202 | layout (location = 0) in vec2 uv; 203 | layout (location = 0) out vec4 color_out; 204 | 205 | void main() 206 | { 207 | vec4 col = get_pixel_col(uv.xy); 208 | 209 | col.rgb = vec3(1.0f, 1.0f, 1.0f) - col.rgb; 210 | 211 | color_out = col; 212 | } 213 | -------------------------------------------------------------------------------- /baldurk/custom-shader-templates/hlsl-template.hlsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | ///////////////////////////////////// 4 | // Constants // 5 | ///////////////////////////////////// 6 | 7 | // possible values (these are only return values from this function, NOT texture binding points): 8 | // RD_TextureType_1D 9 | // RD_TextureType_2D 10 | // RD_TextureType_3D 11 | // RD_TextureType_Depth 12 | // RD_TextureType_DepthStencil 13 | // RD_TextureType_DepthMS 14 | // RD_TextureType_DepthStencilMS 15 | uint RD_TextureType(); 16 | 17 | // selected sample, or -numSamples for resolve 18 | int RD_SelectedSample(); 19 | 20 | uint RD_SelectedSliceFace(); 21 | 22 | uint RD_SelectedMip(); 23 | 24 | // xyz = width, height, depth. w = # mips 25 | uint4 RD_TexDim(); 26 | 27 | // x = horizontal downsample rate (1 full rate, 2 half rate) 28 | // y = vertical downsample rate 29 | // z = number of planes in input texture 30 | // w = number of bits per component (8, 10, 16) 31 | uint4 RD_YUVDownsampleRate(); 32 | 33 | // x = where Y channel comes from 34 | // y = where U channel comes from 35 | // z = where V channel comes from 36 | // w = where A channel comes from 37 | // each index will be [0,1,2,3] for xyzw in first plane, 38 | // [4,5,6,7] for xyzw in second plane texture, etc. 39 | // it will be 0xff = 255 if the channel does not exist. 40 | uint4 RD_YUVAChannels(); 41 | 42 | // a pair with minimum and maximum selected range values 43 | float2 RD_SelectedRange(); 44 | 45 | ///////////////////////////////////// 46 | 47 | ///////////////////////////////////// 48 | // Resources // 49 | ///////////////////////////////////// 50 | 51 | // Float Textures 52 | Texture1DArray texDisplayTex1DArray : register(RD_FLOAT_1D_ARRAY_BINDING); 53 | Texture2DArray texDisplayTex2DArray : register(RD_FLOAT_2D_ARRAY_BINDING); 54 | Texture3D texDisplayTex3D : register(RD_FLOAT_3D_BINDING); 55 | Texture2DMSArray texDisplayTex2DMSArray : register(RD_FLOAT_2DMS_ARRAY_BINDING); 56 | Texture2DArray texDisplayYUVArray : register(RD_FLOAT_YUV_ARRAY_BINDING); 57 | 58 | // only used on D3D 59 | Texture2DArray texDisplayTexDepthArray : register(RD_FLOAT_DEPTH_ARRAY_BINDING); 60 | Texture2DArray texDisplayTexStencilArray : register(RD_FLOAT_STENCIL_ARRAY_BINDING); 61 | Texture2DMSArray texDisplayTexDepthMSArray : register(RD_FLOAT_DEPTHMS_ARRAY_BINDING); 62 | Texture2DMSArray texDisplayTexStencilMSArray : register(RD_FLOAT_STENCILMS_ARRAY_BINDING); 63 | 64 | /* 65 | // Int Textures 66 | Texture1DArray texDisplayIntTex1DArray : register(RD_INT_1D_ARRAY_BINDING); 67 | Texture2DArray texDisplayIntTex2DArray : register(RD_INT_2D_ARRAY_BINDING); 68 | Texture3D texDisplayIntTex3D : register(RD_INT_3D_BINDING); 69 | Texture2DMSArray texDisplayIntTex2DMSArray : register(RD_INT_2DMS_ARRAY_BINDING); 70 | 71 | // Unsigned int Textures 72 | Texture1DArray texDisplayUIntTex1DArray : register(RD_UINT_1D_ARRAY_BINDING); 73 | Texture2DArray texDisplayUIntTex2DArray : register(RD_UINT_2D_ARRAY_BINDING); 74 | Texture3D texDisplayUIntTex3D : register(RD_UINT_3D_BINDING); 75 | Texture2DMSArray texDisplayUIntTex2DMSArray : register(RD_UINT_2DMS_ARRAY_BINDING); 76 | */ 77 | 78 | ///////////////////////////////////// 79 | 80 | ///////////////////////////////////// 81 | // Samplers // 82 | ///////////////////////////////////// 83 | 84 | SamplerState pointSampler : register(RD_POINT_SAMPLER_BINDING); 85 | SamplerState linearSampler : register(RD_LINEAR_SAMPLER_BINDING); 86 | 87 | ///////////////////////////////////// 88 | 89 | 90 | float4 get_pixel_col(float2 uv) 91 | { 92 | float4 col = float4(0,0,0,0); 93 | 94 | // local copy so we can invert/clamp it 95 | int sample = RD_SelectedSample(); 96 | 97 | uint4 texRes = RD_TexDim(); 98 | 99 | uint mip = RD_SelectedMip(); 100 | uint sliceFace = RD_SelectedSliceFace(); 101 | 102 | // RD_TexDim() is always the dimension of the texture. When loading from smaller mips, we need to multiply 103 | // uv by the mip dimension 104 | texRes.x = max(1, texRes.x >> mip); 105 | texRes.y = max(1, texRes.y >> mip); 106 | 107 | if(RD_TextureType() == RD_TextureType_1D) 108 | { 109 | col = texDisplayTex1DArray.Load(int3(uv.x * texRes.x, sliceFace, mip)); 110 | } 111 | else if(RD_TextureType() == RD_TextureType_2D) 112 | { 113 | col = texDisplayTex2DArray.Load(int4(uv.xy * texRes.xy, sliceFace, mip)); 114 | } 115 | else if(RD_TextureType() == RD_TextureType_3D) 116 | { 117 | col = texDisplayTex3D.Load(int4(uv.xy * texRes.xy, sliceFace, mip)); 118 | } 119 | else if(RD_TextureType() == RD_TextureType_Depth) 120 | { 121 | col.r = texDisplayTexDepthArray.Load(int4(uv.xy * texRes.xy, sliceFace, mip)).r; 122 | col.gba = float3(0, 0, 1); 123 | } 124 | else if(RD_TextureType() == RD_TextureType_DepthStencil) 125 | { 126 | col.r = texDisplayTexDepthArray.Load(int4(uv.xy * texRes.xy, sliceFace, mip)).r; 127 | col.g = texDisplayTexStencilArray.Load(int4(uv.xy * texRes.xy, sliceFace, mip)).g / 255.0f; 128 | col.ba = float2(0, 1); 129 | } 130 | else if(RD_TextureType() == RD_TextureType_DepthMS) 131 | { 132 | // sample = -1 means 'average', we'll just return sample 0 133 | if(sample < 0) 134 | sample = 0; 135 | 136 | col.r = texDisplayTexDepthMSArray.Load(int3(uv.xy * texRes.xy, sliceFace), sample).r; 137 | col.gba = float3(0, 0, 1); 138 | } 139 | else if(RD_TextureType() == RD_TextureType_DepthStencilMS) 140 | { 141 | // sample = -1 means 'average', we'll just return sample 0 142 | if(sample < 0) 143 | sample = 0; 144 | 145 | col.r = texDisplayTexDepthMSArray.Load(int3(uv.xy * texRes.xy, sliceFace), sample).r; 146 | col.g = texDisplayTexStencilMSArray.Load(int3(uv.xy * texRes.xy, sliceFace), sample).g / 255.0f; 147 | col.ba = float2(0, 1); 148 | } 149 | else if(RD_TextureType() == RD_TextureType_2DMS) 150 | { 151 | // sample = -1 means 'average' 152 | if(sample < 0) 153 | { 154 | int sampleCount = -sample; 155 | 156 | // worst resolve you've seen in your life 157 | for(int i = 0; i < sampleCount; i++) 158 | col += texDisplayTex2DMSArray.Load(int3(uv.xy * texRes.xy, sliceFace), i); 159 | 160 | col /= float(sampleCount); 161 | } 162 | else 163 | { 164 | col = texDisplayTex2DMSArray.Load(int3(uv.xy * texRes.xy, sliceFace), sample); 165 | } 166 | } 167 | return col; 168 | } 169 | 170 | float4 main(float4 pos : SV_Position, float4 UV : TEXCOORD0) : SV_Target0 171 | { 172 | float4 col = get_pixel_col(UV.xy); 173 | 174 | col.rgb = 1.0f.xxx - col.rgb; 175 | 176 | return col; 177 | } 178 | -------------------------------------------------------------------------------- /baldurk/whereismydraw/README.md: -------------------------------------------------------------------------------- 1 | # Where is my Draw? 2 | 3 | This extension is primarily aimed at newcomers to graphics debugging who might not know where to get started debugging when a drawcall is completely missing. It can be a useful timesaver for anyone though. It will work through a series of checks to try to narrow down why a drawcall is missing. 4 | 5 | If you find a case where a drawcall is missing and you feel like it's something this extension could usefully catch or alert you to then please file an issue with more information. 6 | 7 | This extension is generally built against the latest version of RenderDoc, as the python API can change between versions and ease of readability is valued over backwards compatibility. 8 | 9 | ![Animation showing how the extension works](https://user-images.githubusercontent.com/661798/105998779-e6eb1080-60a4-11eb-8353-f213040e4d22.gif) 10 | 11 | -------------------------------------------------------------------------------- /baldurk/whereismydraw/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2021 Baldur Karlsson 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | ############################################################################### 24 | 25 | import qrenderdoc as qrd 26 | 27 | from . import window 28 | 29 | extiface_version = '' 30 | 31 | 32 | def open_window_callback(ctx: qrd.CaptureContext, data): 33 | win = window.get_window(ctx, extiface_version) 34 | 35 | ctx.RaiseDockWindow(win) 36 | 37 | 38 | def register(version: str, ctx: qrd.CaptureContext): 39 | global extiface_version 40 | extiface_version = version 41 | 42 | print("Registering 'Where is my draw?' extension for RenderDoc version {}".format(version)) 43 | 44 | ctx.Extensions().RegisterWindowMenu(qrd.WindowMenu.Window, ["Where is my Draw?"], open_window_callback) 45 | 46 | 47 | def unregister(): 48 | print("Unregistering 'Where is my draw?' extension") 49 | -------------------------------------------------------------------------------- /baldurk/whereismydraw/analyse.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2021 Baldur Karlsson 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | ############################################################################### 24 | 25 | import qrenderdoc as qrd 26 | import renderdoc as rd 27 | import struct 28 | import math 29 | import random 30 | from typing import Callable, Tuple, List 31 | 32 | 33 | class PixelHistoryData: 34 | def __init__(self): 35 | self.x = 0 36 | self.y = 0 37 | self.id = rd.ResourceId() 38 | self.tex_display = rd.TextureDisplay() 39 | self.history: List[rd.PixelModification] = [] 40 | self.view = rd.ReplayController.NoPreference 41 | self.last_eid = 0 42 | 43 | 44 | class ResultStep: 45 | def __init__(self, *, msg='', tex_display=rd.TextureDisplay(), pixel_history=PixelHistoryData(), 46 | pipe_stage=qrd.PipelineStage.ComputeShader, mesh_view=rd.MeshDataStage.Count): 47 | self.msg = msg 48 | # force copy the input, so it can be modified without changing the one in this step 49 | self.tex_display = rd.TextureDisplay(tex_display) 50 | self.pixel_history = pixel_history 51 | self.pipe_stage = pipe_stage 52 | self.mesh_view = mesh_view 53 | 54 | def has_details(self) -> bool: 55 | return self.tex_display.resourceId != rd.ResourceId() or \ 56 | self.pixel_history.id != rd.ResourceId() or \ 57 | self.pipe_stage != qrd.PipelineStage.ComputeShader or \ 58 | self.mesh_view != rd.MeshDataStage.Count 59 | 60 | 61 | class AnalysisFinished(Exception): 62 | pass 63 | 64 | 65 | class Analysis: 66 | # Do the expensive analysis on the replay thread 67 | def __init__(self, ctx: qrd.CaptureContext, eid: int, r: rd.ReplayController): 68 | self.analysis_steps = [] 69 | self.ctx = ctx 70 | self.eid = eid 71 | self.r = r 72 | 73 | print("On replay thread, analysing @{} with current @{}".format(self.eid, self.ctx.CurEvent())) 74 | 75 | self.r.SetFrameEvent(self.eid, True) 76 | 77 | self.drawcall = self.ctx.GetAction(self.eid) 78 | self.api_properties = self.r.GetAPIProperties() 79 | self.textures = self.r.GetTextures() 80 | self.buffers = self.r.GetBuffers() 81 | self.api = self.api_properties.pipelineType 82 | 83 | self.pipe = self.r.GetPipelineState() 84 | self.glpipe = self.vkpipe = self.d3d11pipe = self.d3d12pipe = None 85 | if self.api == rd.GraphicsAPI.OpenGL: 86 | self.glpipe = self.r.GetGLPipelineState() 87 | elif self.api == rd.GraphicsAPI.Vulkan: 88 | self.vkpipe = self.r.GetVulkanPipelineState() 89 | elif self.api == rd.GraphicsAPI.D3D11: 90 | self.d3d11pipe = self.r.GetD3D11PipelineState() 91 | elif self.api == rd.GraphicsAPI.D3D12: 92 | self.d3d12pipe = self.r.GetD3D12PipelineState() 93 | 94 | self.vert_ndc = [] 95 | 96 | # Enumerate all bound targets, with depth last 97 | self.targets = [t for t in self.pipe.GetOutputTargets() if t.resource != rd.ResourceId.Null()] 98 | self.depth = self.pipe.GetDepthTarget() 99 | if self.depth.resource != rd.ResourceId.Null(): 100 | self.targets.append(self.depth) 101 | 102 | dim = (1, 1) 103 | self.target_descs = [] 104 | for t in self.targets: 105 | desc = self.get_tex(t.resource) 106 | self.target_descs.append(desc) 107 | w = max(dim[0], desc.width) 108 | h = max(dim[1], desc.height) 109 | dim = (w, h) 110 | 111 | self.postvs_stage = rd.MeshDataStage.GSOut 112 | if self.pipe.GetShader(rd.ShaderStage.Geometry) == rd.ResourceId.Null() and self.pipe.GetShader( 113 | rd.ShaderStage.Hull) == rd.ResourceId.Null(): 114 | self.postvs_stage = rd.MeshDataStage.VSOut 115 | 116 | # Gather all the postvs positions together 117 | self.postvs_positions = [] 118 | for inst in range(max(1, self.drawcall.numInstances)): 119 | for view in range(max(1, self.pipe.MultiviewBroadcastCount())): 120 | postvs = self.r.GetPostVSData(inst, view, self.postvs_stage) 121 | pos_data = self.r.GetBufferData(postvs.vertexResourceId, postvs.vertexByteOffset, 122 | postvs.vertexByteStride * postvs.numIndices) 123 | for vert in range(postvs.numIndices): 124 | if vert * postvs.vertexByteStride + 16 < len(pos_data): 125 | self.postvs_positions.append(struct.unpack_from("4f", pos_data, vert * postvs.vertexByteStride)) 126 | 127 | self.vert_ndc = [(vert[0] / vert[3], vert[1] / vert[3], vert[2] / vert[3]) for vert in self.postvs_positions if 128 | vert[3] != 0.0] 129 | 130 | # Create a temporary offscreen output we'll use for 131 | self.out = self.r.CreateOutput(rd.CreateHeadlessWindowingData(dim[0], dim[1]), rd.ReplayOutputType.Texture) 132 | 133 | self.tex_display = rd.TextureDisplay() 134 | 135 | name = self.drawcall.GetName(self.ctx.GetStructuredFile()) 136 | 137 | # We're not actually trying to catch exceptions here, we just want a finally: to shutdown the output 138 | try: 139 | self.analysis_steps = [] 140 | 141 | # If there are no bound targets at all, stop as there's no rendering we can analyse 142 | if len(self.targets) == 0: 143 | self.analysis_steps.append( 144 | ResultStep(msg='No output render targets or depth target are bound at {}.'.format(self.eid))) 145 | 146 | raise AnalysisFinished 147 | 148 | # if the drawcall has a parameter which means no work happens, alert the user 149 | if (self.drawcall.flags & rd.ActionFlags.Instanced) and self.drawcall.numInstances == 0: 150 | self.analysis_steps.append(ResultStep(msg='The drawcall {} is instanced, but the number of instances ' 151 | 'specified is 0.'.format(name))) 152 | 153 | raise AnalysisFinished 154 | 155 | if self.drawcall.numIndices == 0: 156 | vert_name = 'indices' if self.drawcall.flags & rd.ActionFlags.Indexed else 'vertices' 157 | self.analysis_steps.append( 158 | ResultStep(msg='The drawcall {} specifies 0 {}.'.format(name, vert_name))) 159 | 160 | raise AnalysisFinished 161 | 162 | self.check_draw() 163 | except AnalysisFinished: 164 | pass 165 | finally: 166 | self.out.Shutdown() 167 | 168 | self.r.SetFrameEvent(self.eid, False) 169 | 170 | def check_draw(self): 171 | # Render a highlight overlay on the first target. If no color targets are bound this will be the depth 172 | # target. 173 | self.tex_display.resourceId = self.targets[0].resource 174 | self.tex_display.subresource.mip = self.targets[0].firstMip 175 | self.tex_display.subresource.slice = self.targets[0].firstSlice 176 | self.tex_display.typeCast = self.targets[0].format.compType 177 | self.tex_display.scale = -1.0 178 | texmin, texmax = self.r.GetMinMax(self.tex_display.resourceId, self.tex_display.subresource, 179 | self.tex_display.typeCast) 180 | 181 | comp_type = self.tex_display.typeCast 182 | if comp_type is rd.CompType.Typeless: 183 | comp_type = self.target_descs[0].format.compType 184 | 185 | if comp_type == rd.CompType.SInt: 186 | self.tex_display.rangeMin = float(min([texmin.intValue[x] for x in range(4)])) 187 | self.tex_display.rangeMax = float(max([texmax.intValue[x] for x in range(4)])) 188 | elif comp_type == rd.CompType.UInt: 189 | self.tex_display.rangeMin = float(min([texmin.uintValue[x] for x in range(4)])) 190 | self.tex_display.rangeMax = float(max([texmax.uintValue[x] for x in range(4)])) 191 | else: 192 | self.tex_display.rangeMin = min([texmin.floatValue[x] for x in range(4)]) 193 | self.tex_display.rangeMax = max([texmax.floatValue[x] for x in range(4)]) 194 | 195 | texmin, texmax = self.get_overlay_minmax(rd.DebugOverlay.Drawcall) 196 | 197 | if texmax.floatValue[0] < 0.5: 198 | self.analysis_steps.append(ResultStep( 199 | msg='The highlight drawcall overlay shows nothing for this draw, meaning it is off-screen or doesn\'t ' 200 | 'cover enough of a pixel.')) 201 | 202 | self.check_offscreen() 203 | else: 204 | self.analysis_steps.append(ResultStep( 205 | msg='The highlight drawcall overlay shows the draw, meaning it is rendering but failing some tests.', 206 | tex_display=self.tex_display)) 207 | 208 | self.check_onscreen() 209 | 210 | # If we got here, we didn't find a specific problem! Add a note about that 211 | self.analysis_steps.append(ResultStep(msg='Sorry, I couldn\'t prove precisely what was wrong! I\'ve noted the ' 212 | 'steps and checks I took below, as well as anything suspicious I ' 213 | 'found along the way.\n\n' 214 | 'If you think this is something I should have caught with more ' 215 | 'checks please report an issue.')) 216 | 217 | raise AnalysisFinished 218 | 219 | def get_overlay_minmax(self, overlay: rd.DebugOverlay): 220 | self.tex_display.overlay = overlay 221 | self.out.SetTextureDisplay(self.tex_display) 222 | overlay = self.out.GetDebugOverlayTexID() 223 | return self.r.GetMinMax(overlay, rd.Subresource(), rd.CompType.Typeless) 224 | 225 | def get_overlay_histogram(self, tex_display, overlay: rd.DebugOverlay, minmax: Tuple[float, float], 226 | channels: Tuple[bool, bool, bool, bool]): 227 | tex_display.overlay = overlay 228 | self.out.SetTextureDisplay(tex_display) 229 | overlay = self.out.GetDebugOverlayTexID() 230 | return self.r.GetHistogram(overlay, rd.Subresource(), rd.CompType.Typeless, minmax[0], minmax[1], channels) 231 | 232 | def check_onscreen(self): 233 | # It's on-screen we debug the rasterization/testing/blending states 234 | 235 | if self.pipe.GetScissor(0).enabled: 236 | # Check if we're outside scissor first. This overlay is a bit messy because it's not pure red/green, 237 | # so instead of getting the min and max and seeing if there are green pixels, we get the histogram 238 | # because any green will show up in the green channel as distinct from white and black. 239 | texhist = self.get_overlay_histogram(self.tex_display, rd.DebugOverlay.ViewportScissor, (0.0, 1.0), 240 | (False, True, False, False)) 241 | 242 | buckets = list(map(lambda _: _[0], filter(lambda _: _[1] > 0, list(enumerate(texhist))))) 243 | 244 | # drop the top buckets, for white, as well as any buckets lower than 20% for the small amounts of green 245 | # in other colors 246 | buckets = [b for b in buckets if len(texhist) // 5 < b < len(texhist) - 1] 247 | 248 | # If there are no green pixels at all, this completely failed 249 | if len(buckets) == 0: 250 | self.check_failed_scissor() 251 | 252 | # Regardless of whether we finihsed the analysis above, don't do any more checking. 253 | raise 254 | else: 255 | self.analysis_steps.append( 256 | ResultStep(msg='Some or all of the draw passes the scissor test, which is enabled', 257 | tex_display=self.tex_display)) 258 | 259 | texmin, texmax = self.get_overlay_minmax(rd.DebugOverlay.BackfaceCull) 260 | 261 | # If there are no green pixels at all, this completely failed 262 | if texmax.floatValue[1] < 0.5: 263 | self.check_failed_backface_culling() 264 | 265 | # Regardless of whether we finihsed the analysis above, don't do any more checking. 266 | raise AnalysisFinished 267 | else: 268 | self.analysis_steps.append( 269 | ResultStep(msg='Some or all of the draw passes backface culling', 270 | tex_display=self.tex_display)) 271 | 272 | texmin, texmax = self.get_overlay_minmax(rd.DebugOverlay.Depth) 273 | 274 | # If there are no green pixels at all, this completely failed 275 | if texmax.floatValue[1] < 0.5: 276 | self.check_failed_depth() 277 | 278 | # Regardless of whether we finihsed the analysis above, don't do any more checking. 279 | raise AnalysisFinished 280 | else: 281 | self.analysis_steps.append( 282 | ResultStep(msg='Some or all of the draw passes depth testing', 283 | tex_display=self.tex_display)) 284 | 285 | texmin, texmax = self.get_overlay_minmax(rd.DebugOverlay.Stencil) 286 | 287 | # If there are no green pixels at all, this completely failed 288 | if texmax.floatValue[1] < 0.5: 289 | self.check_failed_stencil() 290 | 291 | # Regardless of whether we finihsed the analysis above, don't do any more checking. 292 | raise AnalysisFinished 293 | else: 294 | self.analysis_steps.append( 295 | ResultStep(msg='Some or all of the draw passes stencil testing', 296 | tex_display=self.tex_display)) 297 | 298 | sample_count = self.target_descs[0].msSamp 299 | 300 | # OK we've exhausted the help we can get overlays! 301 | 302 | # Check that the sample mask isn't 0, which will cull the draw 303 | sample_mask = 0xFFFFFFFF 304 | if self.api == rd.GraphicsAPI.OpenGL: 305 | # GL only applies the sample mask in MSAA scenarios 306 | if (sample_count > 1 and self.glpipe.rasterizer.state.multisampleEnable and 307 | self.glpipe.rasterizer.state.sampleMask): 308 | sample_mask = self.glpipe.rasterizer.state.sampleMaskValue 309 | elif self.api == rd.GraphicsAPI.Vulkan: 310 | sample_mask = self.vkpipe.multisample.sampleMask 311 | elif self.api == rd.GraphicsAPI.D3D11: 312 | # D3D always applies the sample mask 313 | sample_mask = self.d3d11pipe.outputMerger.blendState.sampleMask 314 | elif self.api == rd.GraphicsAPI.D3D12: 315 | sample_mask = self.d3d12pipe.rasterizer.sampleMask 316 | 317 | if sample_mask == 0: 318 | self.analysis_steps.append(ResultStep( 319 | msg='The sample mask is set to 0, which will discard all samples.', 320 | pipe_stage=qrd.PipelineStage.SampleMask)) 321 | 322 | raise AnalysisFinished 323 | elif (sample_mask & 0xff) != 0xff: 324 | self.analysis_steps.append( 325 | ResultStep(msg='The sample mask {:08x} is non-zero, meaning at least some samples will ' 326 | 'render.\n\nSome bits are disabled so make sure you check the correct samples.' 327 | .format(sample_mask), 328 | tex_display=self.tex_display)) 329 | else: 330 | self.analysis_steps.append( 331 | ResultStep(msg='The sample mask {:08x} is non-zero or disabled, meaning at least some samples will ' 332 | 'render. ' 333 | .format(sample_mask), 334 | tex_display=self.tex_display)) 335 | 336 | # On GL, check the sample coverage value for MSAA targets 337 | if self.api == rd.GraphicsAPI.OpenGL: 338 | rs_state = self.glpipe.rasterizer.state 339 | if sample_count > 1 and rs_state.multisampleEnable and rs_state.sampleCoverage: 340 | if rs_state.sampleCoverageInvert and rs_state.sampleCoverageValue >= 1.0: 341 | self.analysis_steps.append(ResultStep( 342 | msg='Sample coverage is enabled, set to invert, and the value is {}. This results in a ' 343 | 'coverage mask of 0.'.format(rs_state.sampleCoverageValue), 344 | pipe_stage=qrd.PipelineStage.Rasterizer)) 345 | 346 | raise AnalysisFinished 347 | elif not rs_state.sampleCoverageInvert and rs_state.sampleCoverageValue <= 0.0: 348 | self.analysis_steps.append(ResultStep( 349 | msg='Sample coverage is enabled, and the value is {}. This results in a ' 350 | 'coverage mask of 0.'.format(rs_state.sampleCoverageValue), 351 | pipe_stage=qrd.PipelineStage.Rasterizer)) 352 | 353 | raise AnalysisFinished 354 | else: 355 | self.analysis_steps.append( 356 | ResultStep(msg='The sample coverage value seems to be set correctly.', 357 | tex_display=self.tex_display)) 358 | 359 | blends = self.pipe.GetColorBlends() 360 | targets = self.pipe.GetOutputTargets() 361 | 362 | # Consider a write mask enabled if the corresponding target is unbound, to avoid false positives 363 | enabled_color_masks = [] 364 | color_blends = [] 365 | for i, b in enumerate(blends): 366 | if i >= len(targets) or targets[i].resource == rd.ResourceId.Null(): 367 | color_blends.append(None) 368 | else: 369 | enabled_color_masks.append(b.writeMask != 0) 370 | color_blends.append(b) 371 | 372 | blend_factor = (0.0, 0.0, 0.0, 0.0) 373 | depth_writes = False 374 | if self.api == rd.GraphicsAPI.OpenGL: 375 | blend_factor = self.glpipe.framebuffer.blendState.blendFactor 376 | if self.glpipe.depthState.depthEnable: 377 | depth_writes = self.glpipe.depthState.depthWrites 378 | elif self.api == rd.GraphicsAPI.Vulkan: 379 | blend_factor = self.vkpipe.colorBlend.blendFactor 380 | if self.vkpipe.depthStencil.depthTestEnable: 381 | depth_writes = self.vkpipe.depthStencil.depthWriteEnable 382 | elif self.api == rd.GraphicsAPI.D3D11: 383 | blend_factor = self.d3d11pipe.outputMerger.blendState.blendFactor 384 | if self.d3d11pipe.outputMerger.depthStencilState.depthEnable: 385 | depth_writes = self.d3d11pipe.outputMerger.depthStencilState.depthWrites 386 | elif self.api == rd.GraphicsAPI.D3D12: 387 | blend_factor = self.d3d12pipe.outputMerger.blendState.blendFactor 388 | if self.d3d12pipe.outputMerger.depthStencilState.depthEnable: 389 | depth_writes = self.d3d12pipe.outputMerger.depthStencilState.depthWrites 390 | 391 | # if all color masks are disabled, at least warn - or consider the case solved if depth writes are also disabled 392 | if not any(enabled_color_masks): 393 | if depth_writes: 394 | self.analysis_steps.append(ResultStep( 395 | msg='All bound output targets have a write mask set to 0 - which means no color will be ' 396 | 'written.\n\n ' 397 | 'This may not be the problem if no color output is expected, as depth writes are enabled.', 398 | pipe_stage=qrd.PipelineStage.Blending)) 399 | else: 400 | self.analysis_steps.append(ResultStep( 401 | msg='All bound output targets have a write mask set to 0 - which means no color will be ' 402 | 'written.\n\n ' 403 | 'Depth writes are also disabled so this draw will not output anything.', 404 | pipe_stage=qrd.PipelineStage.Blending)) 405 | 406 | raise AnalysisFinished 407 | 408 | # if only some color masks are disabled, alert the user since they may be wondering why nothing is being output 409 | # to that target 410 | elif not all(enabled_color_masks): 411 | self.analysis_steps.append(ResultStep( 412 | msg='Some output targets have a write mask set to 0 - which means no color will be ' 413 | 'written to those targets.\n\n ' 414 | 'This may not be a problem if no color output is expected on those targets.', 415 | pipe_stage=qrd.PipelineStage.Blending)) 416 | else: 417 | self.analysis_steps.append( 418 | ResultStep(msg='The color write masks seem to be set normally.', 419 | pipe_stage=qrd.PipelineStage.Blending)) 420 | 421 | def is_zero(mul: rd.BlendMultiplier): 422 | if mul == rd.BlendMultiplier.Zero: 423 | return True 424 | if mul == rd.BlendMultiplier.FactorAlpha and blend_factor[3] == 0.0: 425 | return True 426 | if mul == rd.BlendMultiplier.FactorRGB and blend_factor[0:3] == (0.0, 0.0, 0.0): 427 | return True 428 | if mul == rd.BlendMultiplier.InvFactorAlpha and blend_factor[3] == 1.0: 429 | return True 430 | if mul == rd.BlendMultiplier.InvFactorRGB and blend_factor[0:3] == (1.0, 1.0, 1.0): 431 | return True 432 | return False 433 | 434 | def uses_src(mul: rd.BlendMultiplier): 435 | return mul in [rd.BlendMultiplier.SrcCol, rd.BlendMultiplier.InvSrcCol, rd.BlendMultiplier.SrcAlpha, 436 | rd.BlendMultiplier.InvSrcAlpha, 437 | rd.BlendMultiplier.SrcAlphaSat, rd.BlendMultiplier.Src1Col, rd.BlendMultiplier.InvSrc1Col, 438 | rd.BlendMultiplier.Src1Alpha, rd.BlendMultiplier.InvSrc1Alpha] 439 | 440 | # Look for any enabled color blend equations that would work out to 0, or not use the source data 441 | blend_warnings = '' 442 | for i, b in enumerate(color_blends): 443 | if b is not None: 444 | if b.enabled: 445 | # All operations use both operands in some sense so we can't count out any blend equation with just 446 | # that 447 | if is_zero(b.colorBlend.source) and not uses_src(b.colorBlend.destination): 448 | blend_warnings += 'Blending on output {} effectively multiplies the source color by zero, ' \ 449 | 'and the destination color is multiplied by {}, so the source color is ' \ 450 | 'completely unused.'.format(i, b.colorBlend.destination) 451 | 452 | # we don't warn on alpha state since it's sometimes only used for the color 453 | elif b.logicOperationEnabled: 454 | if b.logicOperation == rd.LogicOperation.NoOp: 455 | blend_warnings += 'Blending on output {} is set to use logic operations, and the operation ' \ 456 | 'is no-op.\n'.format(i) 457 | 458 | if blend_warnings != '': 459 | self.analysis_steps.append(ResultStep( 460 | msg='Some color blending state is strange, but not necessarily unintentional. This is worth ' 461 | 'checking if you haven\'t set this up deliberately:\n\n{}'.format(blend_warnings), 462 | pipe_stage=qrd.PipelineStage.Blending)) 463 | elif any([b is not None and b.enabled for b in color_blends]): 464 | self.analysis_steps.append( 465 | ResultStep(msg='The blend equations seem to be set up to allow rendering.', 466 | pipe_stage=qrd.PipelineStage.Blending)) 467 | else: 468 | self.analysis_steps.append( 469 | ResultStep(msg='Blending is disabled.', 470 | pipe_stage=qrd.PipelineStage.Blending)) 471 | 472 | # Nothing else has indicated a problem. Let's try the clear before draw on black and white backgrounds. If we 473 | # see any change that will tell us that the draw is working but perhaps outputting the color that's already 474 | # there. 475 | for t in targets: 476 | if t.resource != rd.ResourceId(): 477 | for sample in range(self.get_tex(t.resource).msSamp): 478 | self.tex_display.resourceId = t.resource 479 | self.tex_display.subresource = rd.Subresource(t.firstMip, t.firstSlice, sample) 480 | self.tex_display.backgroundColor = rd.FloatVector(0.0, 0.0, 0.0, 0.0) 481 | self.tex_display.rangeMin = 0.0 482 | self.tex_display.rangeMax = 1.0 483 | 484 | self.get_overlay_minmax(rd.DebugOverlay.ClearBeforeDraw) 485 | texmin, texmax = self.r.GetMinMax(t.resource, rd.Subresource(), t.format.compType) 486 | 487 | tex_desc = self.get_tex(t.resource) 488 | 489 | c = min(3, tex_desc.format.compCount) 490 | 491 | if any([_ != 0.0 for _ in texmin.floatValue[0:c]]) or \ 492 | any([_ != 0.0 for _ in texmax.floatValue[0:c]]): 493 | self.analysis_steps.append(ResultStep( 494 | msg='The target {} did show a change in RGB when selecting the \'clear before draw\' ' 495 | 'overlay on a black background. Perhaps your shader is outputting the color that is ' 496 | 'already there?'.format(t.resource), 497 | tex_display=self.tex_display)) 498 | 499 | raise AnalysisFinished 500 | 501 | if tex_desc.format.compCount == 4 and (texmin.floatValue[3] != 0.0 or texmax.floatValue[3] != 0.0): 502 | self.analysis_steps.append(ResultStep( 503 | msg='The target {} did show a change in alpha when selecting the \'clear before draw\' ' 504 | 'overlay on a black background. Perhaps your shader is outputting the color that is ' 505 | 'already there, or your blending state isn\'t as expected?'.format(t.resource), 506 | tex_display=self.tex_display)) 507 | 508 | raise AnalysisFinished 509 | 510 | self.tex_display.backgroundColor = rd.FloatVector(1.0, 1.0, 1.0, 1.0) 511 | 512 | self.get_overlay_minmax(rd.DebugOverlay.ClearBeforeDraw) 513 | texmin, texmax = self.r.GetMinMax(t.resource, rd.Subresource(), t.format.compType) 514 | 515 | if any([_ != 1.0 for _ in texmin.floatValue[0:c]]) or \ 516 | any([_ != 1.0 for _ in texmax.floatValue[0:c]]): 517 | self.analysis_steps.append(ResultStep( 518 | msg='The target {} did show a change in RGB when selecting the \'clear before draw\' ' 519 | 'overlay on a white background. Perhaps your shader is outputting the color that is ' 520 | 'already there?'.format(t.resource), 521 | tex_display=self.tex_display)) 522 | 523 | raise AnalysisFinished 524 | 525 | if tex_desc.format.compCount == 4 and (texmin.floatValue[3] != 1.0 or texmax.floatValue[3] != 1.0): 526 | self.analysis_steps.append(ResultStep( 527 | msg='The target {} did show a change in alpha when selecting the \'clear before draw\' ' 528 | 'overlay on a white background. Perhaps your shader is outputting the color that is ' 529 | 'already there, or your blending state isn\'t as expected?'.format(t.resource), 530 | tex_display=self.tex_display)) 531 | 532 | raise AnalysisFinished 533 | 534 | self.analysis_steps.append( 535 | ResultStep(msg='Using the \'clear before draw\' overlay didn\'t show any output on either black or white.')) 536 | 537 | # No obvious failures, if we can run a pixel history let's see if the shader discarded or output something that 538 | # would 539 | if self.api_properties.pixelHistory: 540 | self.tex_display.overlay = rd.DebugOverlay.Drawcall 541 | self.out.SetTextureDisplay(self.tex_display) 542 | overlay = self.out.GetDebugOverlayTexID() 543 | 544 | sub = rd.Subresource(self.tex_display.subresource.mip, 0, 0) 545 | 546 | drawcall_overlay_data = self.r.GetTextureData(overlay, sub) 547 | 548 | dim = self.out.GetDimensions() 549 | 550 | # Scan for all pixels that are covered, since we'll have to try a few 551 | covered_list = [] 552 | for y in range(dim[1]): 553 | for x in range(dim[0]): 554 | pixel_data = struct.unpack_from('4H', drawcall_overlay_data, (y * dim[0] + x) * 8) 555 | if pixel_data[0] != 0: 556 | covered_list.append((x, y)) 557 | 558 | # Shuffle the covered pixels 559 | random.shuffle(covered_list) 560 | 561 | # how many times should we try? Let's go conservative 562 | attempts = 5 563 | discarded_pixels = [] 564 | 565 | attempts = min(attempts, len(covered_list)) 566 | 567 | for attempt in range(attempts): 568 | covered = covered_list[attempt] 569 | 570 | history = self.r.PixelHistory(self.targets[0].resource, covered[0], covered[1], 571 | self.tex_display.subresource, 572 | self.tex_display.typeCast) 573 | 574 | # if we didn't get any hits from this event that's strange but not much we can do about it 575 | if len(history) == 0 or history[-1].eventId != self.eid: 576 | continue 577 | elif history[-1].Passed(): 578 | alpha = history[-1].shaderOut.col.floatValue[3] 579 | blend = color_blends[0] 580 | if blend is None: 581 | blend = rd.ColorBlend() 582 | blend.enabled = False 583 | if alpha <= 0.0 and blend.enabled and blend.colorBlend.source == rd.BlendMultiplier.SrcAlpha: 584 | self.analysis_steps.append(ResultStep( 585 | msg='Running pixel history on {} it showed that a fragment outputted alpha of 0.0.\n\n ' 586 | 'Your blend setup is such that this means the shader output is multiplied by 0')) 587 | else: 588 | self.analysis_steps.append(ResultStep( 589 | msg='Running pixel history on {} it showed that a fragment passed.\n\n ' 590 | 'Double check if maybe the draw is outputting something but it\'s invisible ' 591 | '(e.g. blending to nothing)'.format(covered))) 592 | break 593 | else: 594 | this_draw = [h for h in history if h.eventId == self.eid] 595 | 596 | # We can't really infer anything strongly from this, it's just one pixel. This is just a random 597 | # guess to help guide the user 598 | if all([h.shaderDiscarded for h in this_draw]): 599 | discarded_pixels.append(covered) 600 | 601 | if len(discarded_pixels) > 0: 602 | self.analysis_steps.append(ResultStep( 603 | msg='Pixel history on {} pixels showed that in {} of them all fragments were discarded.\n\n ' 604 | 'This may not mean every other pixel discarded, but it is worth checking in case your ' 605 | 'shader is always discarding.'.format(attempts, len(discarded_pixels)))) 606 | else: 607 | self.analysis_steps.append( 608 | ResultStep(msg='Pixel history didn\'t detect any pixels discarding.')) 609 | else: 610 | self.analysis_steps.append( 611 | ResultStep(msg='Pixel history could narrow down things further but this API doesn\'t support pixel ' 612 | 'history.')) 613 | 614 | def check_failed_scissor(self): 615 | v = self.pipe.GetViewport(0) 616 | s = self.pipe.GetScissor(0) 617 | 618 | s_right = s.x + s.width 619 | s_bottom = s.y + s.height 620 | v_right = v.x + v.width 621 | v_bottom = v.y + v.height 622 | 623 | # if the scissor is empty that's certainly not intentional. 624 | if s.width == 0 or s.height == 0: 625 | self.analysis_steps.append(ResultStep( 626 | msg='The scissor region {},{} to {},{} is empty so nothing will be rendered.' 627 | .format(s.x, s.y, s_right, s_bottom), 628 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 629 | 630 | raise AnalysisFinished 631 | 632 | # If the scissor region doesn't intersect the viewport, that's a problem 633 | if s_right < v.x or s.x > v_right or s.x > v_right or s.y > v_bottom: 634 | self.analysis_steps.append(ResultStep( 635 | msg='The scissor region {},{} to {},{} is completely outside the viewport of {},{} to {},{} so all ' 636 | 'pixels will be scissor clipped'.format(s.x, s.y, s_right, s_bottom, v.x, v.y, v_right, v_bottom), 637 | pipe_stage=qrd.PipelineStage.Rasterizer)) 638 | 639 | raise AnalysisFinished 640 | 641 | self.analysis_steps.append(ResultStep( 642 | msg='The draw is outside of the scissor region, so it has been clipped.\n\n' 643 | 'If this isn\'t intentional, check your scissor state.', 644 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 645 | 646 | raise AnalysisFinished 647 | 648 | def check_offscreen(self): 649 | v = self.pipe.GetViewport(0) 650 | 651 | if v.width <= 1.0 or abs(v.height) <= 1.0: 652 | self.analysis_steps.append( 653 | ResultStep(msg='Viewport 0 is {}x{} so nothing will be rendered.'.format(v.width, v.height), 654 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 655 | 656 | raise AnalysisFinished 657 | elif v.x >= self.target_descs[0].width or v.y >= self.target_descs[0].height: 658 | self.analysis_steps.append( 659 | ResultStep(msg='Viewport 0 is placed at {},{} which is out of bounds for the target dimension {}x{}.' 660 | .format(v.x, v.y, self.target_descs[0].width, self.target_descs[0].height), 661 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 662 | 663 | raise AnalysisFinished 664 | else: 665 | self.analysis_steps.append( 666 | ResultStep(msg='Viewport 0 looks normal, it\'s {}x{} at {},{}.'.format(v.width, v.height, v.x, v.y), 667 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 668 | 669 | if self.api == rd.GraphicsAPI.Vulkan: 670 | ra = self.vkpipe.currentPass.renderArea 671 | 672 | # if the render area is empty that's certainly not intentional. 673 | if ra.width == 0 or ra.height == 0: 674 | self.analysis_steps.append( 675 | ResultStep(msg='The render area is {}x{} so nothing will be rendered.'.format(ra.width, ra.height), 676 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 677 | 678 | raise AnalysisFinished 679 | 680 | # Other invalid render areas outside of attachment dimensions would be invalid behaviour that renderdoc 681 | # doesn't account for 682 | else: 683 | self.analysis_steps.append(ResultStep( 684 | msg='The vulkan render area {}x{} at {},{} is fine.'.format(ra.width, ra.height, ra.x, ra.y), 685 | pipe_stage=qrd.PipelineStage.Rasterizer)) 686 | 687 | # Check rasterizer discard state 688 | if (self.glpipe is not None and self.glpipe.vertexProcessing.discard) or ( 689 | self.vkpipe is not None and self.vkpipe.rasterizer.rasterizerDiscardEnable): 690 | self.analysis_steps.append(ResultStep( 691 | msg='Rasterizer discard is enabled. This API state disables rasterization for the drawcall.', 692 | pipe_stage=qrd.PipelineStage.Rasterizer)) 693 | 694 | raise AnalysisFinished 695 | elif self.glpipe is not None or self.vkpipe is not None: 696 | self.analysis_steps.append(ResultStep( 697 | msg='Rasterizer discard is not enabled, so that should be fine.', 698 | pipe_stage=qrd.PipelineStage.Rasterizer)) 699 | 700 | # Check position was written to 701 | vsrefl = self.pipe.GetShaderReflection(rd.ShaderStage.Vertex) 702 | dsrefl = self.pipe.GetShaderReflection(rd.ShaderStage.Domain) 703 | gsrefl = self.pipe.GetShaderReflection(rd.ShaderStage.Geometry) 704 | lastrefl = None 705 | 706 | if lastrefl is None: 707 | lastrefl = gsrefl 708 | if lastrefl is None: 709 | lastrefl = dsrefl 710 | if lastrefl is None: 711 | lastrefl = vsrefl 712 | 713 | if lastrefl is None: 714 | self.analysis_steps.append(ResultStep( 715 | msg='No vertex, tessellation or geometry shader is bound.', 716 | mesh_view=self.postvs_stage)) 717 | 718 | raise AnalysisFinished 719 | 720 | pos_found = False 721 | for sig in lastrefl.outputSignature: 722 | if sig.systemValue == rd.ShaderBuiltin.Position: 723 | pos_found = True 724 | 725 | if not pos_found: 726 | self.analysis_steps.append(ResultStep( 727 | msg='The last post-transform shader {} does not write to the position builtin.' 728 | .format(lastrefl.resourceId), 729 | mesh_view=self.postvs_stage)) 730 | 731 | raise AnalysisFinished 732 | 733 | if len(self.vert_ndc) == 0 and len(self.postvs_positions) != 0: 734 | self.analysis_steps.append(ResultStep( 735 | msg='All of the post-transform vertex positions have W=0.0 which is invalid, you should check your ' 736 | 'vertex tranformation setup.', 737 | mesh_view=self.postvs_stage)) 738 | elif len(self.vert_ndc) < len(self.postvs_positions): 739 | self.analysis_steps.append(ResultStep( 740 | msg='Some of the post-transform vertex positions have W=0.0 which is invalid, you should check your ' 741 | 'vertex tranformation setup.', 742 | mesh_view=self.postvs_stage)) 743 | 744 | vert_ndc_x = list(filter(lambda _: math.isfinite(_), [vert[0] for vert in self.vert_ndc])) 745 | vert_ndc_y = list(filter(lambda _: math.isfinite(_), [vert[1] for vert in self.vert_ndc])) 746 | 747 | if len(vert_ndc_x) == 0 or len(vert_ndc_y) == 0: 748 | self.analysis_steps.append(ResultStep( 749 | msg='The post-transform vertex positions are all NaN or infinity, when converted to normalised device ' 750 | 'co-ordinates (NDC) by dividing XYZ by W.', 751 | mesh_view=self.postvs_stage)) 752 | 753 | self.check_invalid_verts() 754 | 755 | raise AnalysisFinished 756 | 757 | v_min = [min(vert_ndc_x), min(vert_ndc_y)] 758 | v_max = [max(vert_ndc_x), max(vert_ndc_y)] 759 | 760 | # We can't really easily write a definitive algorithm to determine "reasonable transform, but offscreen" and 761 | # "broken transform". As a heuristic we see if the bounds are within a reasonable range of the NDC box 762 | # (broken floats are very likely to be outside this range) and that the area of the bounds is at least one pixel 763 | # (if the input data is broken/empty the vertices may all be transformed to a point). 764 | 765 | # project the NDC min/max onto the viewport and see how much of a pixel it covers 766 | v = self.pipe.GetViewport(0) 767 | top_left = ((v_min[0] * 0.5 + 0.5) * v.width, (v_min[1]*0.5 + 0.5) * v.height) 768 | bottom_right = ((v_max[0] * 0.5 + 0.5) * v.width, (v_max[1]*0.5 + 0.5) * v.height) 769 | 770 | area = (bottom_right[0] - top_left[0]) * (bottom_right[1] - top_left[1]) 771 | 772 | # if the area is below a pixel but we're in the clip region, this might just be a tiny draw or it might be 773 | # broken 774 | if 0.0 < area < 1.0 and v_min[0] >= -1.0 and v_min[1] >= -1.0 and v_max[0] <= 1.0 and v_max[1] <= 1.0: 775 | self.analysis_steps.append(ResultStep( 776 | msg='The calculated area covered by this draw is only {} of a pixel, meaning this draw may be too small' 777 | 'to render.'.format(area), 778 | mesh_view=self.postvs_stage)) 779 | 780 | self.check_invalid_verts() 781 | else: 782 | # if we ARE off screen but we're within 10% of the guard band (which is already *huge*) and 783 | # the area is bigger than a pixel then we assume this is a normal draw that's just off screen. 784 | if max([abs(_) for _ in v_min + v_max]) < 32767.0 / 10.0 and area > 1.0: 785 | self.analysis_steps.append(ResultStep( 786 | msg='The final position outputs from the vertex shading stages looks reasonable but off-screen.\n\n' 787 | 'Check that your transformation and vertex shading is working as expected, or perhaps this ' 788 | 'drawcall should be off-screen.', 789 | mesh_view=self.postvs_stage)) 790 | 791 | raise AnalysisFinished 792 | # if we're in the outer regions of the guard band or the area is tiny, assume broken and check for invalid 793 | # inputs if we can 794 | else: 795 | self.analysis_steps.append(ResultStep( 796 | msg='The final position outputs seem to be invalid or degenerate, when converted to normalised ' 797 | 'device co-ordinates (NDC) by dividing XYZ by W.', 798 | mesh_view=self.postvs_stage)) 799 | 800 | self.check_invalid_verts() 801 | 802 | def check_invalid_verts(self): 803 | vs = self.pipe.GetShader(rd.ShaderStage.Vertex) 804 | 805 | # There should be at least a vertex shader bound 806 | if vs == rd.ResourceId.Null(): 807 | self.analysis_steps.append(ResultStep( 808 | msg='No valid vertex shader is bound.', 809 | pipe_stage=qrd.PipelineStage.VertexShader)) 810 | 811 | raise AnalysisFinished 812 | 813 | prev_len = len(self.analysis_steps) 814 | 815 | # if there's an index buffer bound, we'll bounds check it then calculate the indices 816 | if self.drawcall.flags & rd.ActionFlags.Indexed: 817 | ib = self.pipe.GetIBuffer() 818 | if ib.resourceId == rd.ResourceId.Null() or ib.byteStride == 0: 819 | self.analysis_steps.append(ResultStep( 820 | msg='This draw is indexed, but there is no valid index buffer bound.', 821 | pipe_stage=qrd.PipelineStage.VertexInput)) 822 | 823 | raise AnalysisFinished 824 | 825 | ibSize = ib.byteSize 826 | ibOffs = ib.byteOffset + self.drawcall.indexOffset * ib.byteStride 827 | # if the binding is unbounded, figure out how much is left in the buffer 828 | if ibSize == 0xFFFFFFFFFFFFFFFF: 829 | buf = self.get_buf(ib.resourceId) 830 | if buf is None or ibOffs > buf.length: 831 | ibSize = 0 832 | else: 833 | ibSize = buf.length - ibOffs 834 | 835 | ibNeededSize = self.drawcall.numIndices * ib.byteStride 836 | if ibSize < ibNeededSize: 837 | explanation = 'The index buffer is bound with a {} byte range'.format(ib.byteSize) 838 | if ib.byteSize == 0xFFFFFFFFFFFFFFFF: 839 | buf = self.get_buf(ib.resourceId) 840 | if buf is None: 841 | buf = rd.BufferDescription() 842 | explanation = '' 843 | explanation += 'The index buffer is {} bytes in size.\n'.format(buf.length) 844 | explanation += 'It is bound with an offset of {}.\n'.format(ib.byteOffset) 845 | explanation += 'The drawcall specifies an offset of {} indices (each index is {} bytes)\n'.format( 846 | self.drawcall.indexOffset, ib.byteStride) 847 | explanation += 'Meaning only {} bytes are available'.format(ibSize) 848 | 849 | self.analysis_steps.append(ResultStep( 850 | msg='This draw reads {} {}-byte indices from {}, meaning total {} bytes are needed, but ' 851 | 'only {} bytes are available. This is unlikely to be intentional.\n\n{}' 852 | .format(self.drawcall.numIndices, ib.byteStride, ib.resourceId, ibNeededSize, 853 | ibSize, explanation), 854 | pipe_stage=qrd.PipelineStage.VertexInput)) 855 | 856 | read_bytes = min(ibSize, ibNeededSize) 857 | 858 | # Fetch the data 859 | if read_bytes > 0: 860 | ibdata = self.r.GetBufferData(ib.resourceId, ibOffs, read_bytes) 861 | else: 862 | ibdata = bytes() 863 | 864 | # Get the character for the width of index 865 | index_fmt = 'B' 866 | if ib.byteStride == 2: 867 | index_fmt = 'H' 868 | elif ib.byteStride == 4: 869 | index_fmt = 'I' 870 | 871 | avail_indices = int(len(ibdata) / ib.byteStride) 872 | 873 | # Duplicate the format by the number of indices 874 | index_fmt = '=' + str(min(avail_indices, self.drawcall.numIndices)) + index_fmt 875 | 876 | # Unpack all the indices 877 | indices = struct.unpack_from(index_fmt, ibdata) 878 | 879 | restart_idx = self.pipe.GetRestartIndex() & ((1 << (ib.byteStride*8)) - 1) 880 | restart_enabled = self.pipe.IsRestartEnabled() and rd.IsStrip(self.pipe.GetPrimitiveTopology()) 881 | 882 | # Detect restart indices and map them to None, otherwise apply basevertex 883 | indices = [None if restart_enabled and i == restart_idx else i + self.drawcall.baseVertex for i in indices] 884 | else: 885 | indices = [i + self.drawcall.vertexOffset for i in range(self.drawcall.numIndices)] 886 | 887 | # what's the maximum index? for bounds checking 888 | max_index = max(indices) 889 | max_index_idx = indices.index(max_index) 890 | max_inst = max(self.drawcall.numInstances - 1, 0) 891 | 892 | vsinputs = self.pipe.GetVertexInputs() 893 | vbuffers = self.pipe.GetVBuffers() 894 | avail = [0] * len(vbuffers) 895 | 896 | # Determine the available bytes in each vertex buffer 897 | for i, vb in enumerate(vbuffers): 898 | vbSize = vb.byteSize 899 | vbOffs = vb.byteOffset 900 | # if the binding is unbounded, figure out how much is left in the buffer 901 | if vbSize == 0xFFFFFFFFFFFFFFFF: 902 | buf = self.get_buf(vb.resourceId) 903 | if buf is None or vbOffs > buf.length: 904 | vbSize = 0 905 | else: 906 | vbSize = buf.length - vbOffs 907 | avail[i] = vbSize 908 | 909 | # bounds check each attribute against the maximum available 910 | for attr in vsinputs: 911 | if not attr.used: 912 | continue 913 | 914 | if attr.vertexBuffer >= len(vbuffers) or vbuffers[attr.vertexBuffer].resourceId == rd.ResourceId.Null(): 915 | self.analysis_steps.append(ResultStep( 916 | msg='Vertex attribute {} references vertex buffer slot {} which has no buffer bound.' 917 | .format(attr.name, attr.vertexBuffer), 918 | pipe_stage=qrd.PipelineStage.VertexInput)) 919 | 920 | continue 921 | 922 | vb: rd.BoundVBuffer = vbuffers[attr.vertexBuffer] 923 | 924 | avail_bytes = avail[attr.vertexBuffer] 925 | used_bytes = attr.format.ElementSize() 926 | 927 | if attr.perInstance: 928 | max_inst_offs = max(self.drawcall.instanceOffset + max_inst, 0) 929 | 930 | if attr.byteOffset + max_inst_offs * vb.byteStride + used_bytes > avail_bytes: 931 | explanation = '' 932 | explanation += 'The vertex buffer {} has {} bytes available'.format(attr.vertexBuffer, avail_bytes) 933 | 934 | if vb.byteSize == 0xFFFFFFFFFFFFFFFF: 935 | buf = self.get_buf(vb.resourceId) 936 | if buf is None: 937 | buf = rd.BufferDescription() 938 | explanation += ' because it is {} bytes long, ' \ 939 | 'and is bound at offset {} bytes'.format(buf.length, vb.byteOffset) 940 | explanation += '.\n' 941 | 942 | explanation += 'The maximum instance index is {}'.format(max_inst) 943 | if self.drawcall.instanceOffset > 0: 944 | explanation += ' (since the draw renders {} instances starting at {})'.format( 945 | self.drawcall.numInstances, self.drawcall.instanceOffset) 946 | explanation += '.\n' 947 | 948 | explanation += 'Meaning the highest offset read from is {}.\n'.format(max_inst_offs * vb.byteStride) 949 | explanation += 'The attribute reads {} bytes at offset {} from that.\n'.format(used_bytes, 950 | attr.byteOffset) 951 | 952 | self.analysis_steps.append(ResultStep( 953 | msg='Per-instance vertex attribute {} reads out of bounds on vertex buffer slot {}:\n\n{}' 954 | .format(attr.name, attr.vertexBuffer, explanation), 955 | pipe_stage=qrd.PipelineStage.VertexInput)) 956 | else: 957 | max_idx_offs = max(self.drawcall.baseVertex + max_index, 0) 958 | 959 | if attr.byteOffset + max_idx_offs * vb.byteStride + used_bytes > avail_bytes: 960 | explanation = '' 961 | explanation += 'The vertex buffer {} has {} bytes available'.format(attr.vertexBuffer, avail_bytes) 962 | 963 | if vb.byteSize == 0xFFFFFFFFFFFFFFFF: 964 | buf = self.get_buf(vb.resourceId) 965 | if buf is None: 966 | buf = rd.BufferDescription() 967 | explanation += ' because it is {} bytes long, ' \ 968 | 'and is bound at offset {} bytes'.format(buf.length, vb.byteOffset) 969 | explanation += '.\n' 970 | 971 | if self.drawcall.flags & rd.ActionFlags.Indexed: 972 | explanation += 'The maximum index is {} (found at vertex {}'.format(max_index, 973 | max_index_idx) 974 | base = self.drawcall.baseVertex 975 | if base != 0: 976 | explanation += ' by adding base vertex {} to index {}'.format(base, max_index - base) 977 | explanation += ').\n' 978 | else: 979 | explanation += 'The maximum vertex is {}'.format(max_index) 980 | if self.drawcall.vertexOffset > 0: 981 | explanation += ' (since the draw renders {} vertices starting at {})'.format( 982 | self.drawcall.numIndices, self.drawcall.vertexOffset) 983 | explanation += '.\n' 984 | 985 | explanation += 'Meaning the highest offset read from is {}.\n'.format(max_idx_offs * vb.byteStride) 986 | explanation += 'The attribute reads {} bytes at offset {} from that.\n'.format(used_bytes, 987 | attr.byteOffset) 988 | 989 | self.analysis_steps.append(ResultStep( 990 | msg='Per-vertex vertex attribute {} reads out of bounds on vertex buffer slot {}:\n\n{}' 991 | .format(attr.name, attr.vertexBuffer, explanation), 992 | pipe_stage=qrd.PipelineStage.VertexInput)) 993 | 994 | # This is a bit of a desperation move but it might help some people. Look for any matrix parameters that 995 | # are obviously broken because they're all 0.0. Don't look inside structs or arrays because they might be 996 | # optional/unused 997 | 998 | vsrefl = self.pipe.GetShaderReflection(rd.ShaderStage.Vertex) 999 | 1000 | for cb in self.pipe.GetConstantBlocks(rd.ShaderStage.Vertex): 1001 | if vsrefl.constantBlocks[cb.access.index].bindArraySize <= 1: 1002 | cb_vars = self.r.GetCBufferVariableContents(self.pipe.GetGraphicsPipelineObject(), vs, 1003 | rd.ShaderStage.Vertex, 1004 | self.pipe.GetShaderEntryPoint(rd.ShaderStage.Vertex), 1005 | cb.access.index, cb.descriptor.resource, 1006 | cb.descriptor.byteOffset, cb.descriptor.byteSize) 1007 | 1008 | for v in cb_vars: 1009 | if v.rows > 1 and v.columns > 1: 1010 | suspicious = True 1011 | value = '' 1012 | 1013 | if v.type == rd.VarType.Float: 1014 | vi = 0 1015 | for r in range(v.rows): 1016 | for c in range(v.columns): 1017 | if v.value.f32v[vi] != 0.0: 1018 | suspicious = False 1019 | value += '{:.3}'.format(v.value.f32v[vi]) 1020 | vi += 1 1021 | if c < v.columns - 1: 1022 | value += ', ' 1023 | value += '\n' 1024 | elif v.type == rd.VarType.Half: 1025 | vi = 0 1026 | for r in range(v.rows): 1027 | for c in range(v.columns): 1028 | x = rd.HalfToFloat(v.value.u16v[vi]) 1029 | if x != 0.0: 1030 | suspicious = False 1031 | value += '{:.3}'.format(x) 1032 | vi += 1 1033 | if c < v.columns - 1: 1034 | value += ', ' 1035 | value += '\n' 1036 | elif v.type == rd.VarType.Double: 1037 | vi = 0 1038 | for r in range(v.rows): 1039 | for c in range(v.columns): 1040 | if v.value.f64v[vi] != 0.0: 1041 | suspicious = False 1042 | value += '{:.3}'.format(v.value.f64v[vi]) 1043 | vi += 1 1044 | if c < v.columns - 1: 1045 | value += ', ' 1046 | value += '\n' 1047 | 1048 | if suspicious: 1049 | self.analysis_steps.append(ResultStep( 1050 | msg='Vertex constant {} in {} is an all-zero matrix which looks suspicious.\n\n{}' 1051 | .format(v.name, vsrefl.constantBlocks[i].name, value), 1052 | pipe_stage=qrd.PipelineStage.VertexShader)) 1053 | 1054 | # In general we can't know what the user will be doing with their vertex inputs to generate output, so we can't 1055 | # say that any input setup is "wrong". However we can certainly try and guess at the problem, so we look for 1056 | # any and all attributes with 'pos' in the name, and see if they're all zeroes or all identical 1057 | for attr in vsinputs: 1058 | if not attr.used: 1059 | continue 1060 | 1061 | if 'pos' not in attr.name.lower(): 1062 | continue 1063 | 1064 | if attr.vertexBuffer >= len(vbuffers): 1065 | continue 1066 | 1067 | vb: rd.BoundVBuffer = vbuffers[attr.vertexBuffer] 1068 | 1069 | if vb.resourceId == rd.ResourceId.Null(): 1070 | continue 1071 | 1072 | if attr.perInstance: 1073 | self.analysis_steps.append(ResultStep( 1074 | msg='Attribute \'{}\' is set to be per-instance. If this is a vertex ' 1075 | 'position attribute then that might be unintentional.'.format(attr.name), 1076 | pipe_stage=qrd.PipelineStage.VertexInput)) 1077 | else: 1078 | max_idx_offs = max(self.drawcall.baseVertex + max_index - 1, 0) 1079 | data = self.r.GetBufferData(vb.resourceId, 1080 | vb.byteOffset + attr.byteOffset + max_idx_offs * vb.byteStride, 0) 1081 | 1082 | elem_size = attr.format.ElementSize() 1083 | vert_bytes = [] 1084 | offs = 0 1085 | while offs + elem_size <= len(data): 1086 | vert_bytes.append(data[offs:offs+elem_size]) 1087 | offs += vb.byteStride 1088 | 1089 | if len(vert_bytes) > 1: 1090 | # get the unique set of vertices 1091 | unique_vertices = list(set(vert_bytes)) 1092 | 1093 | # get all usages of the buffer before this event 1094 | buf_usage = [u for u in self.r.GetUsage(vb.resourceId) if u.eventId < self.eid] 1095 | 1096 | # trim to only write usages 1097 | write_usages = [rd.ResourceUsage.VS_RWResource, rd.ResourceUsage.HS_RWResource, 1098 | rd.ResourceUsage.DS_RWResource, rd.ResourceUsage.GS_RWResource, 1099 | rd.ResourceUsage.PS_RWResource, rd.ResourceUsage.CS_RWResource, 1100 | rd.ResourceUsage.All_RWResource, 1101 | rd.ResourceUsage.Copy, rd.ResourceUsage.StreamOut, 1102 | rd.ResourceUsage.CopyDst, rd.ResourceUsage.Discard, rd.ResourceUsage.CPUWrite] 1103 | buf_usage = [u for u in buf_usage if u.usage in write_usages] 1104 | 1105 | if len(buf_usage) >= 1: 1106 | buffer_last_mod = '{} was last modified with {} at @{}, you could check that it wrote ' \ 1107 | 'what you expected.'.format(vb.resourceId, buf_usage[-1].usage, 1108 | buf_usage[-1].eventId) 1109 | else: 1110 | buffer_last_mod = '{} hasn\'t been modified in this capture, check that you initialised it ' \ 1111 | 'with the correct data or wrote it before the beginning of the ' \ 1112 | 'capture.'.format(vb.resourceId) 1113 | 1114 | # If all vertices are 0s, give a more specific error message 1115 | if not any([any(v) for v in unique_vertices]): 1116 | self.analysis_steps.append(ResultStep( 1117 | msg='Attribute \'{}\' all members are zero. ' 1118 | 'If this is a vertex position attribute then that might be unintentional.\n\n{}' 1119 | .format(attr.name, buffer_last_mod), 1120 | mesh_view=rd.MeshDataStage.VSIn)) 1121 | # otherwise error if we only saw one vertex (maybe it's all 0xcccccccc or something) 1122 | elif len(unique_vertices) <= 1: 1123 | self.analysis_steps.append(ResultStep( 1124 | msg='Attribute \'{}\' all members are identical. ' 1125 | 'If this is a vertex position attribute then that might be unintentional.\n\n{}' 1126 | .format(attr.name, buffer_last_mod), 1127 | mesh_view=rd.MeshDataStage.VSIn)) 1128 | 1129 | if len(self.analysis_steps) == prev_len: 1130 | self.analysis_steps.append( 1131 | ResultStep(msg='Didn\'t find any problems with the vertex input setup!')) 1132 | 1133 | def check_failed_backface_culling(self): 1134 | cull_mode = rd.CullMode.NoCull 1135 | front = 'Front CW' 1136 | if self.api == rd.GraphicsAPI.OpenGL: 1137 | cull_mode = self.glpipe.rasterizer.state.cullMode 1138 | if self.glpipe.rasterizer.state.frontCCW: 1139 | front = 'Front: CCW' 1140 | elif self.api == rd.GraphicsAPI.Vulkan: 1141 | cull_mode = self.vkpipe.rasterizer.cullMode 1142 | if self.vkpipe.rasterizer.frontCCW: 1143 | front = 'Front: CCW' 1144 | elif self.api == rd.GraphicsAPI.D3D11: 1145 | cull_mode = self.d3d11pipe.rasterizer.state.cullMode 1146 | if self.d3d11pipe.rasterizer.state.frontCCW: 1147 | front = 'Front: CCW' 1148 | elif self.api == rd.GraphicsAPI.D3D12: 1149 | cull_mode = self.d3d12pipe.rasterizer.state.cullMode 1150 | if self.d3d12pipe.rasterizer.state.frontCCW: 1151 | front = 'Front: CCW' 1152 | 1153 | self.analysis_steps.append(ResultStep( 1154 | msg='The backface culling overlay shows red, so the draw is completely backface culled.\n\n' 1155 | 'Check your polygon winding ({}) and front-facing state ({}).'.format(front, str(cull_mode)), 1156 | tex_display=self.tex_display)) 1157 | 1158 | raise AnalysisFinished 1159 | 1160 | def check_failed_depth(self): 1161 | self.analysis_steps.append(ResultStep( 1162 | msg='The depth test overlay shows red, so the draw is completely failing a depth test.', 1163 | tex_display=self.tex_display)) 1164 | 1165 | v = self.pipe.GetViewport(0) 1166 | 1167 | # Gather API-specific state 1168 | depth_func = rd.CompareFunction.AlwaysTrue 1169 | ndc_bounds = [0.0, 1.0] 1170 | depth_bounds = [] 1171 | depth_enabled = False 1172 | depth_clamp = True 1173 | if self.api == rd.GraphicsAPI.OpenGL: 1174 | depth_enabled = self.glpipe.depthState.depthEnable 1175 | if self.glpipe.depthState.depthBounds: 1176 | depth_bounds = [self.glpipe.depthState.nearBound, self.glpipe.depthState.farBound] 1177 | depth_func = self.glpipe.depthState.depthFunction 1178 | depth_clamp = self.glpipe.rasterizer.state.depthClamp 1179 | if self.glpipe.vertexProcessing.clipNegativeOneToOne: 1180 | ndc_bounds = [-1.0, 1.0] 1181 | elif self.api == rd.GraphicsAPI.Vulkan: 1182 | depth_enabled = self.vkpipe.depthStencil.depthTestEnable 1183 | if self.vkpipe.depthStencil.depthBoundsEnable: 1184 | depth_bounds = [self.vkpipe.depthStencil.minDepthBounds, 1185 | self.vkpipe.depthStencil.maxDepthBounds] 1186 | depth_func = self.vkpipe.depthStencil.depthFunction 1187 | depth_clamp = self.vkpipe.rasterizer.depthClampEnable 1188 | elif self.api == rd.GraphicsAPI.D3D11: 1189 | depth_enabled = self.d3d11pipe.outputMerger.depthStencilState.depthEnable 1190 | depth_func = self.d3d11pipe.outputMerger.depthStencilState.depthFunction 1191 | depth_clamp = not self.d3d11pipe.rasterizer.state.depthClip 1192 | elif self.api == rd.GraphicsAPI.D3D12: 1193 | depth_enabled = self.d3d12pipe.outputMerger.depthStencilState.depthEnable 1194 | if self.d3d12pipe.outputMerger.depthStencilState.depthBoundsEnable: 1195 | depth_bounds = [self.d3d12pipe.outputMerger.depthStencilState.minDepthBounds, 1196 | self.d3d12pipe.outputMerger.depthStencilState.maxDepthBounds] 1197 | depth_func = self.d3d12pipe.outputMerger.depthStencilState.depthFunction 1198 | depth_clamp = not self.d3d12pipe.rasterizer.state.depthClip 1199 | 1200 | # Check for state setups that will always fail 1201 | if depth_func == rd.CompareFunction.Never: 1202 | self.analysis_steps.append(ResultStep( 1203 | msg='Depth test is set to Never, meaning it always fails for this draw.', 1204 | pipe_stage=qrd.PipelineStage.DepthTest)) 1205 | 1206 | raise AnalysisFinished 1207 | 1208 | # Calculate the min/max NDC bounds of the vertices in z 1209 | vert_ndc_z = list(filter(lambda _: math.isfinite(_), [vert[2] for vert in self.vert_ndc])) 1210 | vert_bounds = [min(vert_ndc_z), max(vert_ndc_z)] 1211 | 1212 | self.analysis_steps.append(ResultStep( 1213 | msg='From the vertex output data I calculated the vertices lie within {:.4} and {:.4} in NDC z' 1214 | .format(vert_bounds[0], vert_bounds[1]), 1215 | pipe_stage=qrd.PipelineStage.Rasterizer)) 1216 | 1217 | state_name = 'Depth Clip' if rd.IsD3D(self.api) else 'Depth Clamp' 1218 | 1219 | # if depth clipping is enabled (aka depth clamping is disabled), this happens regardless of if 1220 | # depth testing is enabled 1221 | if not depth_clamp: 1222 | # If the largest vertex NDC z is lower than the NDC range, the whole draw is near-plane clipped 1223 | if vert_bounds[1] < ndc_bounds[0]: 1224 | self.analysis_steps.append(ResultStep( 1225 | msg='All of the drawcall vertices are in front of the near plane, and the ' 1226 | 'current {} state means these vertices get clipped.'.format(state_name), 1227 | mesh_view=self.postvs_stage)) 1228 | 1229 | raise AnalysisFinished 1230 | else: 1231 | self.analysis_steps.append(ResultStep( 1232 | msg='At least some of the vertices are on the passing side of the near plane', 1233 | mesh_view=self.postvs_stage)) 1234 | 1235 | # Same for the smallest z being above the NDC range 1236 | if vert_bounds[0] > ndc_bounds[1]: 1237 | self.analysis_steps.append(ResultStep( 1238 | msg='All of the drawcall vertices are behind the far plane, and the ' 1239 | 'current {} state means these vertices get clipped.'.format(state_name), 1240 | mesh_view=self.postvs_stage)) 1241 | 1242 | raise AnalysisFinished 1243 | else: 1244 | self.analysis_steps.append(ResultStep( 1245 | msg='At least some of the vertices are on the passing side of the far plane', 1246 | mesh_view=self.postvs_stage)) 1247 | else: 1248 | self.analysis_steps.append(ResultStep( 1249 | msg='The current {} state means the near/far planes are ignored for clipping'.format(state_name))) 1250 | 1251 | # all other checks should only run if depth test is enabled 1252 | if not depth_enabled: 1253 | self.analysis_steps.append(ResultStep( 1254 | msg='Depth test stage is disabled! Normally this means the depth test should always pass.\n\n' 1255 | 'Sorry I couldn\'t figure out the exact problem. Please check your {} ' 1256 | 'setup and report an issue so we can narrow this down in future.', 1257 | pipe_stage=qrd.PipelineStage.DepthTest)) 1258 | 1259 | raise AnalysisFinished 1260 | 1261 | # Check that the viewport depth range doesn't trivially fail depth bounds 1262 | if depth_bounds and (v.minDepth > depth_bounds[1] or v.maxDepth < depth_bounds[0]): 1263 | self.analysis_steps.append(ResultStep( 1264 | msg='The viewport depth range ({} to {}) are outside the depth bounds range ({} to {}), ' 1265 | 'which is enabled'.format(v.minDepth, v.maxDepth, depth_bounds[0], depth_bounds[1]), 1266 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1267 | 1268 | raise AnalysisFinished 1269 | elif depth_bounds: 1270 | self.analysis_steps.append(ResultStep( 1271 | msg='The viewport depth range ({} to {}) is within the depth bounds range ({} to {})' 1272 | .format(v.minDepth, v.maxDepth, depth_bounds[0], depth_bounds[1]), 1273 | mesh_view=self.postvs_stage)) 1274 | 1275 | # If the vertex NDC z range does not intersect the depth bounds range, and depth bounds test is 1276 | # enabled, the draw fails the depth bounds test 1277 | if depth_bounds and (vert_bounds[0] > depth_bounds[1] or vert_bounds[1] < depth_bounds[0]): 1278 | self.analysis_steps.append(ResultStep( 1279 | msg='All of the drawcall vertices are outside the depth bounds range ({} to {}), ' 1280 | 'which is enabled'.format(depth_bounds[0], depth_bounds[1]), 1281 | pipe_stage=qrd.PipelineStage.Rasterizer)) 1282 | 1283 | raise AnalysisFinished 1284 | elif depth_bounds: 1285 | self.analysis_steps.append(ResultStep( 1286 | msg='Some vertices are within the depth bounds range ({} to {})' 1287 | .format(v.minDepth, v.maxDepth, depth_bounds[0], depth_bounds[1]), 1288 | mesh_view=self.postvs_stage)) 1289 | 1290 | # Equal depth testing is often used but not equal is rare - flag it too 1291 | if depth_func == rd.CompareFunction.NotEqual: 1292 | self.analysis_steps.append(ResultStep( 1293 | msg='The depth function of {} is not a problem but is unusual.'.format(depth_func), 1294 | pipe_stage=qrd.PipelineStage.DepthTest)) 1295 | 1296 | if v.minDepth != 0.0 or v.maxDepth != 1.0: 1297 | self.analysis_steps.append(ResultStep( 1298 | msg='The viewport min and max depth are set to {} and {}, which is unusual.'.format(v.minDepth, 1299 | v.maxDepth), 1300 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1301 | else: 1302 | self.analysis_steps.append( 1303 | ResultStep(msg='The viewport depth bounds are {} to {} which is normal.'.format(v.minDepth, v.maxDepth), 1304 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1305 | 1306 | self.check_previous_depth_stencil(depth_func) 1307 | 1308 | def check_previous_depth_stencil(self, depth_func): 1309 | val_name = 'depth' if depth_func is not None else 'stencil' 1310 | test_name = '{} test'.format(val_name) 1311 | result_stage = qrd.PipelineStage.DepthTest if depth_func is not None else qrd.PipelineStage.StencilTest 1312 | 1313 | # If no depth buffer is bound, all APIs spec that depth/stencil test should always pass! This seems 1314 | # quite strange. 1315 | if self.depth.resource == rd.ResourceId.Null(): 1316 | self.analysis_steps.append(ResultStep( 1317 | msg='No depth buffer is bound! Normally this means the {} should always pass.\n\n' 1318 | 'Sorry I couldn\'t figure out the exact problem. Please check your {} ' 1319 | 'setup and report an issue so we can narrow this down in future.'.format(test_name, test_name), 1320 | pipe_stage=result_stage)) 1321 | 1322 | raise AnalysisFinished 1323 | 1324 | # Get the last clear of the current depth buffer 1325 | usage = self.r.GetUsage(self.depth.resource) 1326 | 1327 | # Filter for clears before this event 1328 | usage = [u for u in usage if u.eventId < self.eid and u.usage == rd.ResourceUsage.Clear] 1329 | 1330 | # If there's a prior clear 1331 | if len(usage) > 0: 1332 | clear_eid = usage[-1].eventId 1333 | 1334 | self.r.SetFrameEvent(clear_eid, True) 1335 | 1336 | # On GL the scissor test affects clears, check that 1337 | if self.api == rd.GraphicsAPI.OpenGL: 1338 | tmp_glpipe = self.r.GetGLPipelineState() 1339 | s = tmp_glpipe.rasterizer.scissors[0] 1340 | if s.enabled: 1341 | v = self.pipe.GetViewport(0) 1342 | 1343 | s_right = s.x + s.width 1344 | s_bottom = s.y + s.height 1345 | v_right = v.x + v.width 1346 | v_bottom = v.y + v.height 1347 | 1348 | # if the scissor is empty or outside the size of the target that's certainly not intentional. 1349 | if s.width == 0 or s.height == 0: 1350 | self.analysis_steps.append(ResultStep( 1351 | msg='The last depth-stencil clear of {} at {} had scissor enabled, but the scissor rect ' 1352 | '{},{} to {},{} is empty so nothing will get cleared.' 1353 | .format(str(self.depth.resource), clear_eid, s.x, s.y, s_right, s_bottom), 1354 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1355 | 1356 | if s.x >= self.target_descs[-1].width or s.y >= self.target_descs[-1].height: 1357 | self.analysis_steps.append(ResultStep( 1358 | msg='The last depth-stencil clear of {} at {} had scissor enabled, but the scissor rect ' 1359 | '{},{} to {},{} doesn\'t cover the depth-stencil target so it won\'t get cleared.' 1360 | .format(str(self.depth.resource), clear_eid, s.x, s.y, s_right, s_bottom), 1361 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1362 | 1363 | # if the clear's scissor doesn't overlap the viewport at the time of the draw, 1364 | # warn the user 1365 | elif v.x < s.x or v.y < s.y or v.x + v_right or v_bottom > s_bottom: 1366 | self.analysis_steps.append(ResultStep( 1367 | msg='The last depth-stencil clear of {} at {} had scissor enabled, but the scissor rect ' 1368 | '{},{} to {},{} is smaller than the current viewport {},{} to {},{}. ' 1369 | 'This may mean not every pixel was properly cleared.' 1370 | .format(str(self.depth.resource), clear_eid, s.x, s.y, s_right, s_bottom, v.x, v.y, 1371 | v_right, v_bottom), 1372 | pipe_stage=qrd.PipelineStage.ViewportsScissors)) 1373 | 1374 | # If this was a clear then we expect the depth value to be uniform, so pick the pixel to 1375 | # get the depth clear value. 1376 | clear_color = self.r.PickPixel(self.depth.resource, 0, 0, 1377 | rd.Subresource(self.depth.firstMip, self.depth.firstSlice, 0), 1378 | self.depth.format.compType) 1379 | 1380 | self.r.SetFrameEvent(self.eid, True) 1381 | 1382 | if depth_func is not None: 1383 | if clear_eid > 0 and ( 1384 | clear_color.floatValue[0] == 1.0 and depth_func == rd.CompareFunction.Greater) or ( 1385 | clear_color.floatValue[0] == 0.0 and depth_func == rd.CompareFunction.Less): 1386 | self.analysis_steps.append(ResultStep( 1387 | msg='The last depth clear of {} at @{} cleared depth to {:.4}, but the depth comparison ' 1388 | 'function is {} which is impossible to pass.'.format(str(self.depth.resource), 1389 | clear_eid, 1390 | clear_color.floatValue[0], 1391 | str(depth_func).split('.')[-1]), 1392 | pipe_stage=qrd.PipelineStage.DepthTest)) 1393 | 1394 | raise AnalysisFinished 1395 | 1396 | v = self.pipe.GetViewport(0) 1397 | 1398 | if clear_eid > 0 and ((clear_color.floatValue[0] >= max(v.minDepth, v.maxDepth) and 1399 | depth_func == rd.CompareFunction.Greater) or 1400 | (clear_color.floatValue[0] <= min(v.minDepth, v.maxDepth) and 1401 | depth_func == rd.CompareFunction.Less) or 1402 | (clear_color.floatValue[0] > min(v.minDepth, v.maxDepth) and 1403 | depth_func == rd.CompareFunction.GreaterEqual) or 1404 | (clear_color.floatValue[0] < min(v.minDepth, v.maxDepth) and 1405 | depth_func == rd.CompareFunction.LessEqual)): 1406 | self.analysis_steps.append(ResultStep( 1407 | msg='The last depth clear of {} at @{} cleared depth to {:.4}, but the viewport ' 1408 | 'min/max bounds ({:.4} to {:.4}) mean this draw can\'t compare {}.' 1409 | .format(str(self.depth.resource), clear_eid, clear_color.floatValue[0], v.minDepth, 1410 | v.maxDepth, str(depth_func).split('.')[-1]), 1411 | pipe_stage=qrd.PipelineStage.DepthTest)) 1412 | 1413 | raise AnalysisFinished 1414 | 1415 | # This isn't necessarily an error but is unusual - flag it 1416 | if clear_eid > 0 and ( 1417 | clear_color.floatValue[0] == 1.0 and depth_func == rd.CompareFunction.GreaterEqual) or ( 1418 | clear_color.floatValue[0] == 0.0 and depth_func == rd.CompareFunction.LessEqual): 1419 | self.analysis_steps.append(ResultStep( 1420 | msg='The last depth clear of {} at EID {} cleared depth to {}, but the depth comparison ' 1421 | 'function is {} which is highly unlikely to pass. This is worth checking' 1422 | .format(str(self.depth.resource), clear_eid, clear_color.floatValue[0], 1423 | str(depth_func).split('.')[-1]), 1424 | pipe_stage=qrd.PipelineStage.DepthTest)) 1425 | else: 1426 | self.analysis_steps.append(ResultStep( 1427 | msg='The last depth clear of {} at @{} cleared depth to {}, which is reasonable.' 1428 | .format(str(self.depth.resource), clear_eid, clear_color.floatValue[0]))) 1429 | 1430 | # If there's no depth/stencil clear found at all, that's a red flag 1431 | else: 1432 | self.analysis_steps.append(ResultStep( 1433 | msg='The depth-stencil target was not cleared prior to this draw, so it may contain unexpected ' 1434 | 'contents.')) 1435 | 1436 | # Nothing seems obviously broken, this draw might just be occluded. See if we can get some pixel 1437 | # history results to confirm or guide the user 1438 | if self.api_properties.pixelHistory: 1439 | self.tex_display.overlay = rd.DebugOverlay.Drawcall 1440 | self.out.SetTextureDisplay(self.tex_display) 1441 | overlay = self.out.GetDebugOverlayTexID() 1442 | 1443 | sub = rd.Subresource(self.tex_display.subresource.mip, 0, 0) 1444 | 1445 | drawcall_overlay_data = self.r.GetTextureData(overlay, sub) 1446 | 1447 | dim = self.out.GetDimensions() 1448 | 1449 | # Scan for a pixel that's covered 1450 | covered = None 1451 | for y in range(dim[1]): 1452 | for x in range(dim[0]): 1453 | pixel_data = struct.unpack_from('4H', drawcall_overlay_data, (y * dim[0] + x) * 8) 1454 | if pixel_data[0] != 0: 1455 | covered = (x, y) 1456 | break 1457 | if covered is not None: 1458 | break 1459 | 1460 | if covered: 1461 | sub = rd.Subresource(self.targets[-1].firstMip, self.targets[-1].firstSlice) 1462 | history = self.r.PixelHistory(self.targets[-1].resource, covered[0], covered[1], sub, 1463 | self.targets[-1].format.compType) 1464 | 1465 | if len(history) == 0 or history[-1].eventId != self.eid or history[-1].Passed(): 1466 | self.analysis_steps.append(ResultStep( 1467 | msg='I tried to run pixel history on the draw to get more information but on {} ' 1468 | 'I didn\'t get valid results!\n\n ' 1469 | 'This is a bug, please report it so it can be investigated.'.format(covered))) 1470 | else: 1471 | this_draw = [h for h in history if h.eventId == self.eid] 1472 | pre_draw_val = this_draw[0].preMod.depth if depth_func is not None else this_draw[0].preMod.stencil 1473 | last_draw_eid = 0 1474 | for h in reversed(history): 1475 | # Skip this draw itself 1476 | if h.eventId == self.eid: 1477 | continue 1478 | 1479 | # Skip any failed events 1480 | if not h.Passed(): 1481 | continue 1482 | 1483 | if depth_func is not None: 1484 | if h.preMod.depth != pre_draw_val and h.postMod.depth == pre_draw_val: 1485 | last_draw_eid = h.eventId 1486 | break 1487 | else: 1488 | if h.preMod.stencil != pre_draw_val and h.postMod.stencil == pre_draw_val: 1489 | last_draw_eid = h.eventId 1490 | break 1491 | 1492 | history_package = PixelHistoryData() 1493 | history_package.x = covered[0] 1494 | history_package.y = covered[1] 1495 | history_package.id = self.targets[-1].resource 1496 | history_package.tex_display = rd.TextureDisplay(self.tex_display) 1497 | history_package.tex_display.resourceId = self.targets[-1].resource 1498 | history_package.tex_display.subresource = sub 1499 | history_package.tex_display.typeCast = self.targets[-1].format.compType 1500 | history_package.last_eid = last_draw_eid 1501 | history_package.view = rd.ReplayController.NoPreference 1502 | history_package.history = history 1503 | 1504 | if last_draw_eid > 0: 1505 | self.analysis_steps.append(ResultStep( 1506 | msg='Pixel history on {} showed that {} fragments were outputted but their {} ' 1507 | 'values all failed against the {} before the draw of {:.4}.\n\n ' 1508 | 'The draw which outputted that depth value is at @{}.' 1509 | .format(covered, len(this_draw), val_name, val_name, pre_draw_val, last_draw_eid), 1510 | pixel_history=history_package)) 1511 | else: 1512 | self.analysis_steps.append(ResultStep( 1513 | msg='Pixel history on {} showed that {} fragments outputted but their {} ' 1514 | 'values all failed against the {} before the draw of {:.4}.\n\n ' 1515 | 'No previous draw was detected that wrote that {} value.' 1516 | .format(covered, len(this_draw), val_name, val_name, pre_draw_val, val_name), 1517 | pixel_history=history_package)) 1518 | else: 1519 | self.analysis_steps.append(ResultStep( 1520 | msg='I tried to run pixel history on the draw to get more information but couldn\'t ' 1521 | 'find a pixel covered!\n\n ' 1522 | 'This is a bug, please report it so it can be investigated.')) 1523 | 1524 | self.tex_display.overlay = rd.DebugOverlay.Depth if depth_func is not None else rd.DebugOverlay.Stencil 1525 | 1526 | self.analysis_steps.append(ResultStep( 1527 | msg='This drawcall appears to be failing the {} normally. Check to see what else ' 1528 | 'rendered before it, and whether it should be occluded or if something else is in the ' 1529 | 'way.'.format(test_name), 1530 | tex_display=self.tex_display)) 1531 | 1532 | def check_failed_stencil(self): 1533 | self.analysis_steps.append(ResultStep( 1534 | msg='The stencil test overlay shows red, so the draw is completely failing a stencil test.', 1535 | tex_display=self.tex_display)) 1536 | 1537 | # Get the cull mode. If culling is enabled we know which stencil state is in use and can narrow our analysis, 1538 | # if culling is disabled then unfortunately we can't automatically narrow down which side is used. 1539 | cull_mode = rd.CullMode.NoCull 1540 | stencil_enabled = False 1541 | front = back = rd.StencilFace() 1542 | if self.api == rd.GraphicsAPI.OpenGL: 1543 | cull_mode = self.glpipe.rasterizer.state.cullMode 1544 | stencil_enabled = self.glpipe.stencilState.stencilEnable 1545 | front = self.glpipe.stencilState.frontFace 1546 | back = self.glpipe.stencilState.backFace 1547 | elif self.api == rd.GraphicsAPI.Vulkan: 1548 | cull_mode = self.vkpipe.rasterizer.cullMode 1549 | stencil_enabled = self.vkpipe.depthStencil.stencilTestEnable 1550 | front = self.vkpipe.depthStencil.frontFace 1551 | back = self.vkpipe.depthStencil.backFace 1552 | elif self.api == rd.GraphicsAPI.D3D11: 1553 | cull_mode = self.d3d11pipe.rasterizer.state.cullMode 1554 | stencil_enabled = self.d3d11pipe.outputMerger.depthStencilState.stencilEnable 1555 | front = self.d3d11pipe.outputMerger.depthStencilState.frontFace 1556 | back = self.d3d11pipe.outputMerger.depthStencilState.backFace 1557 | elif self.api == rd.GraphicsAPI.D3D12: 1558 | cull_mode = self.d3d12pipe.rasterizer.state.cullMode 1559 | stencil_enabled = self.d3d12pipe.outputMerger.depthStencilState.stencilEnable 1560 | front = self.d3d12pipe.outputMerger.depthStencilState.frontFace 1561 | back = self.d3d12pipe.outputMerger.depthStencilState.backFace 1562 | 1563 | # To simplify code, we're going to check if both faces are the same anyway so if one side is being culled we 1564 | # just pretend that face has the same state as the other (which isn't culled) 1565 | if cull_mode == rd.CullMode.Front: 1566 | front = back 1567 | elif cull_mode == rd.CullMode.Back: 1568 | back = front 1569 | 1570 | if not stencil_enabled: 1571 | self.analysis_steps.append(ResultStep( 1572 | msg='Depth test stage is disabled! Normally this means the depth test should always ' 1573 | 'pass.\n\n' 1574 | 'Sorry I couldn\'t figure out the exact problem. Please check your {} ' 1575 | 'setup and report an issue so we can narrow this down in future.', 1576 | pipe_stage=qrd.PipelineStage.DepthTest)) 1577 | 1578 | raise AnalysisFinished 1579 | 1580 | # Each of these checks below will check for two cases: first that the states are the same between front and 1581 | # back, meaning EITHER that both were the same in the application so we don't need to know whether front or 1582 | # back faces are in the draw, OR that one face is being culled so after we've eliminated a backface culling 1583 | # possibility a stencil failure must be from the other face. 1584 | # 1585 | # In this first case, we can be sure of the problem. 1586 | # 1587 | # In the second case we check if one of the states matches, in which case we can't be sure of the problem but 1588 | # we can alert the users about it. This potentially has false positives if e.g. someone doesn't set backface 1589 | # culling but also doesn't configure the backface stencil state. 1590 | 1591 | def check_faces(msg: str, check: Callable[[rd.StencilFace], None]): 1592 | checks = check(front), check(back) 1593 | 1594 | if all(checks): 1595 | self.analysis_steps.append(ResultStep(msg=msg.format(test='test', s=front), 1596 | pipe_stage=qrd.PipelineStage.StencilTest)) 1597 | 1598 | raise AnalysisFinished 1599 | elif checks[0]: 1600 | msg += ' If your draw relies on back faces then this could be the problem.' 1601 | self.analysis_steps.append(ResultStep(msg=msg.format(test='back face test', s=front), 1602 | pipe_stage=qrd.PipelineStage.StencilTest)) 1603 | elif checks[1]: 1604 | msg += ' If your draw relies on front faces then this could be the problem.' 1605 | self.analysis_steps.append(ResultStep(msg=msg.format(test='front face test', s=back), 1606 | pipe_stage=qrd.PipelineStage.StencilTest)) 1607 | 1608 | # Check simple cases that can't ever be true 1609 | check_faces('The stencil {test} is set to Never, meaning it always fails.', 1610 | lambda x: x.function == rd.CompareFunction.Never) 1611 | check_faces('The stencil {test} is set to {s.function} than {s.reference}, which is impossible.', 1612 | lambda x: (x.function == rd.CompareFunction.Less and x.reference == 0) or ( 1613 | x.function == rd.CompareFunction.Greater and x.reference == 255)) 1614 | check_faces('The stencil {test} is set to {s.function} than {s.reference}, which is impossible.', 1615 | lambda x: (x.function == rd.CompareFunction.LessEqual and x.reference < 0) or ( 1616 | x.function == rd.CompareFunction.GreaterEqual and x.reference > 255)) 1617 | 1618 | # compareMask being 0 is almost certainly a problem, but we can't *prove* it except in certain circumstances. 1619 | # e.g. having a compareMask of 0 and a reference of 0 would pass, or less than a non-zero reference. 1620 | # Fortunately, most of the cases we can prove will catch common errors. At least errors that cause a draw to 1621 | # not show up. 1622 | 1623 | # if the compareMask is set such that the reference value can never be achieved, that's a guaranteed failure 1624 | check_faces( 1625 | 'The stencil {test} is set to compare equal to {s.reference}, but the compare mask is {s.compareMask:x} ' 1626 | 'meaning it never can.', 1627 | lambda x: x.function == rd.CompareFunction.Equal and ( 1628 | (x.compareMask & x.reference) != x.reference) and x.reference != 0) 1629 | 1630 | # The compareMask is the largest value that can be read, if the test is such that only larger values would pass, 1631 | # that's also broken. 1632 | check_faces( 1633 | 'The stencil {test} is set to compare greater than {s.reference}, but the compare mask is ' 1634 | '{s.compareMask:x} meaning it never can.', 1635 | lambda x: x.function == rd.CompareFunction.Greater and x.compareMask <= x.reference) 1636 | check_faces( 1637 | 'The stencil {test} is set to compare greater than or equal to {s.reference}, but the compare mask is ' 1638 | '{s.compareMask:x} meaning it never can.', 1639 | lambda x: x.function == rd.CompareFunction.GreaterEqual and x.compareMask < x.reference) 1640 | 1641 | # Equal stencil testing is often used but not equal is rare - flag it too 1642 | try: 1643 | check_faces('The stencil {test} is set to Not Equal, which is not a problem but is unusual.', 1644 | lambda x: x.function == rd.CompareFunction.Never) 1645 | except AnalysisFinished: 1646 | # we're not actually finished even if both faces were not equal! 1647 | pass 1648 | 1649 | self.check_previous_depth_stencil(None) 1650 | 1651 | def get_steps(self): 1652 | return self.analysis_steps 1653 | 1654 | def get_tex(self, resid: rd.ResourceId): 1655 | for t in self.textures: 1656 | if t.resourceId == resid: 1657 | return t 1658 | return None 1659 | 1660 | def get_buf(self, resid: rd.ResourceId): 1661 | for b in self.buffers: 1662 | if b.resourceId == resid: 1663 | return b 1664 | return None 1665 | 1666 | 1667 | def analyse_draw(ctx: qrd.CaptureContext, eid: int, finished_callback): 1668 | # define a local function that wraps the detail of needing to invoke back/forth onto replay thread 1669 | def _replay_callback(r: rd.ReplayController): 1670 | analysis = Analysis(ctx, eid, r) 1671 | 1672 | # Invoke back onto the UI thread to display the results 1673 | ctx.Extensions().GetMiniQtHelper().InvokeOntoUIThread(lambda: finished_callback(analysis.get_steps())) 1674 | 1675 | ctx.Replay().AsyncInvoke('where_is_my_draw', _replay_callback) 1676 | -------------------------------------------------------------------------------- /baldurk/whereismydraw/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_api": 1, 3 | "name": "Where is my draw?", 4 | "version": "1.0", 5 | "minimum_renderdoc": "1.13", 6 | "description": "This extension adds a new 'Where is my draw?' panel under Window which can be used to diagnose why a draw isn't appearing.\n\nThis is primarily aimed at new graphics developers who might not know what to look for, but it may be a timesaver for anyone.", 7 | "author": "Baldur Karlsson ", 8 | "url": "https://github.com/baldurk/renderdoc-contrib" 9 | } 10 | -------------------------------------------------------------------------------- /baldurk/whereismydraw/window.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2021 Baldur Karlsson 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | ############################################################################### 24 | 25 | import qrenderdoc as qrd 26 | import renderdoc as rd 27 | from typing import Optional 28 | from . import analyse 29 | 30 | mqt: qrd.MiniQtHelper 31 | 32 | 33 | def format_mod(mod: rd.ModificationValue): 34 | if mod.stencil < 0: 35 | return 'Depth: {:.4}\n'.format(mod.depth) 36 | else: 37 | return 'Depth: {:.4} Stencil: {:02x}\n'.format(mod.depth, mod.stencil) 38 | 39 | 40 | class Window(qrd.CaptureViewer): 41 | def __init__(self, ctx: qrd.CaptureContext, version: str): 42 | super().__init__() 43 | 44 | print("Creating WIMD window") 45 | 46 | self.ctx = ctx 47 | self.version = version 48 | self.topWindow = mqt.CreateToplevelWidget("Where is my Draw?", lambda c, w, d: closed()) 49 | 50 | vert = mqt.CreateVerticalContainer() 51 | mqt.AddWidget(self.topWindow, vert) 52 | 53 | self.analyseButton = mqt.CreateButton(lambda c, w, d: self.start_analysis()) 54 | self.analyseLabel = mqt.CreateLabel() 55 | # Add inside a horizontal container to left align it 56 | horiz = mqt.CreateHorizontalContainer() 57 | mqt.AddWidget(horiz, self.analyseButton) 58 | mqt.AddWidget(horiz, self.analyseLabel) 59 | mqt.AddWidget(horiz, mqt.CreateSpacer(True)) 60 | mqt.AddWidget(vert, horiz) 61 | 62 | self.results = mqt.CreateGroupBox(False) 63 | mqt.SetWidgetText(self.results, "Results") 64 | mqt.AddWidget(vert, self.results) 65 | 66 | vert = mqt.CreateVerticalContainer() 67 | mqt.AddWidget(self.results, vert) 68 | self.summaryText = mqt.CreateLabel() 69 | self.stepText = mqt.CreateLabel() 70 | self.texOutWidget = mqt.CreateOutputRenderingWidget() 71 | self.meshOutWidget = mqt.CreateOutputRenderingWidget() 72 | self.resultsSpacer = mqt.CreateSpacer(False) 73 | self.navSpacer = mqt.CreateSpacer(True) 74 | self.resultsNavigationBar = mqt.CreateHorizontalContainer() 75 | self.resultsPrev = mqt.CreateButton(lambda c, w, d: self.goto_previous_step()) 76 | mqt.SetWidgetText(self.resultsPrev, "Previous Step") 77 | self.resultsNext = mqt.CreateButton(lambda c, w, d: self.goto_next_step()) 78 | mqt.SetWidgetText(self.resultsNext, "Next Step") 79 | self.showDetails = mqt.CreateButton(lambda c, w, d: self.goto_details()) 80 | mqt.SetWidgetText(self.showDetails, "Show More Info") 81 | mqt.AddWidget(self.resultsNavigationBar, self.resultsPrev) 82 | mqt.AddWidget(self.resultsNavigationBar, self.resultsNext) 83 | mqt.AddWidget(self.resultsNavigationBar, self.showDetails) 84 | mqt.AddWidget(self.resultsNavigationBar, self.navSpacer) 85 | mqt.AddWidget(vert, self.summaryText) 86 | mqt.AddWidget(vert, self.resultsNavigationBar) 87 | mqt.AddWidget(vert, self.stepText) 88 | mqt.AddWidget(vert, self.texOutWidget) 89 | mqt.AddWidget(vert, self.meshOutWidget) 90 | mqt.AddWidget(vert, self.resultsSpacer) 91 | 92 | self.texOut: rd.ReplayOutput 93 | self.texOut = None 94 | self.meshOut: rd.ReplayOutput 95 | self.meshOut = None 96 | 97 | self.eid = 0 98 | 99 | self.cur_result = 0 100 | self.results = [] 101 | 102 | # Reset state using this to avoid duplicated logic 103 | self.OnCaptureClosed() 104 | 105 | ctx.AddDockWindow(self.topWindow, qrd.DockReference.MainToolArea, None) 106 | ctx.AddCaptureViewer(self) 107 | 108 | def OnCaptureLoaded(self): 109 | self.reset() 110 | 111 | tex_data: rd.WindowingData = mqt.GetWidgetWindowingData(self.texOutWidget) 112 | mesh_data: rd.WindowingData = mqt.GetWidgetWindowingData(self.meshOutWidget) 113 | 114 | def set_widgets(): 115 | mqt.SetWidgetReplayOutput(self.texOutWidget, self.texOut) 116 | mqt.SetWidgetReplayOutput(self.meshOutWidget, self.meshOut) 117 | 118 | def make_out(r: rd.ReplayController): 119 | self.texOut = r.CreateOutput(tex_data, rd.ReplayOutputType.Texture) 120 | self.meshOut = r.CreateOutput(mesh_data, rd.ReplayOutputType.Texture) 121 | mqt.InvokeOntoUIThread(set_widgets) 122 | 123 | self.ctx.Replay().AsyncInvoke('', make_out) 124 | 125 | def OnCaptureClosed(self): 126 | self.reset() 127 | 128 | def reset(self): 129 | mqt.SetWidgetText(self.analyseButton, "Analyse draw") 130 | mqt.SetWidgetEnabled(self.analyseButton, False) 131 | 132 | mqt.SetWidgetVisible(self.summaryText, True) 133 | mqt.SetWidgetVisible(self.stepText, False) 134 | mqt.SetWidgetVisible(self.resultsNavigationBar, False) 135 | mqt.SetWidgetVisible(self.texOutWidget, False) 136 | mqt.SetWidgetVisible(self.meshOutWidget, False) 137 | mqt.SetWidgetVisible(self.resultsSpacer, True) 138 | 139 | mqt.SetWidgetText(self.summaryText, "No analysis available.") 140 | mqt.SetWidgetText(self.stepText, "") 141 | 142 | self.cur_result = 0 143 | self.results = [] 144 | 145 | def shutdown_out(r: rd.ReplayController): 146 | if self.texOut is not None: 147 | self.texOut.Shutdown() 148 | if self.meshOut is not None: 149 | self.meshOut.Shutdown() 150 | self.texOut = None 151 | self.meshOut = None 152 | 153 | self.ctx.Replay().AsyncInvoke('', shutdown_out) 154 | 155 | def closed(self): 156 | self.reset() 157 | 158 | def OnSelectedEventChanged(self, event): 159 | pass 160 | 161 | def OnEventChanged(self, event): 162 | draw = self.ctx.GetAction(event) 163 | 164 | if draw is not None and (draw.flags & rd.ActionFlags.Drawcall): 165 | mqt.SetWidgetText(self.analyseButton, "Analyse draw {}: {}".format(draw.eventId, self.get_action_name(draw))) 166 | mqt.SetWidgetEnabled(self.analyseButton, True) 167 | else: 168 | mqt.SetWidgetText(self.analyseButton, "Can't analyse {}, select a draw".format(event)) 169 | mqt.SetWidgetEnabled(self.analyseButton, False) 170 | 171 | self.refresh_result() 172 | 173 | def get_action_name(self, draw: rd.ActionDescription): 174 | return draw.GetName(self.ctx.GetStructuredFile()) 175 | 176 | def start_analysis(self): 177 | self.eid = self.ctx.CurEvent() 178 | print("Analysing {}".format(self.eid)) 179 | mqt.SetWidgetEnabled(self.analyseButton, False) 180 | 181 | mqt.SetWidgetText(self.summaryText, "Analysis in progress, please wait!") 182 | mqt.SetWidgetText(self.stepText, "") 183 | mqt.SetWidgetVisible(self.summaryText, True) 184 | mqt.SetWidgetVisible(self.stepText, False) 185 | mqt.SetWidgetVisible(self.resultsNavigationBar, False) 186 | mqt.SetWidgetVisible(self.texOutWidget, False) 187 | mqt.SetWidgetVisible(self.meshOutWidget, False) 188 | mqt.SetWidgetVisible(self.resultsSpacer, True) 189 | 190 | analyse.analyse_draw(self.ctx, self.eid, lambda results: self.finish_analysis(results)) 191 | 192 | def finish_analysis(self, results): 193 | print("Analysis finished") 194 | mqt.SetWidgetEnabled(self.analyseButton, True) 195 | 196 | mqt.SetWidgetVisible(self.stepText, True) 197 | mqt.SetWidgetVisible(self.resultsNavigationBar, True) 198 | 199 | self.results = results 200 | self.cur_result = 0 201 | 202 | draw = self.ctx.GetAction(self.eid) 203 | 204 | if len(self.results) == 0: 205 | mqt.SetWidgetText(self.summaryText, 206 | "Analysis failed for {}: {}!".format(self.eid, self.get_action_name(draw))) 207 | else: 208 | mqt.SetWidgetText(self.summaryText, "Conclusion of analysis for {}: {}:\n\n{}" 209 | .format(self.eid, self.get_action_name(draw), self.format_step_text(-1))) 210 | 211 | self.refresh_result() 212 | 213 | def goto_previous_step(self): 214 | self.cur_result = max(self.cur_result - 1, 0) 215 | 216 | self.refresh_result() 217 | 218 | def goto_next_step(self): 219 | self.cur_result = min(self.cur_result + 1, len(self.results) - 1) 220 | 221 | self.refresh_result() 222 | 223 | def refresh_result(self): 224 | if len(self.results) == 0: 225 | mqt.SetWidgetText(self.summaryText, "No results available.") 226 | mqt.SetWidgetText(self.stepText, "") 227 | mqt.SetWidgetVisible(self.summaryText, True) 228 | mqt.SetWidgetVisible(self.stepText, False) 229 | mqt.SetWidgetVisible(self.resultsNavigationBar, False) 230 | mqt.SetWidgetVisible(self.texOutWidget, False) 231 | mqt.SetWidgetVisible(self.meshOutWidget, False) 232 | mqt.SetWidgetVisible(self.resultsSpacer, True) 233 | return 234 | 235 | step = analyse.ResultStep() 236 | if self.cur_result in range(len(self.results)): 237 | step = self.results[self.cur_result] 238 | 239 | mqt.SetWidgetEnabled(self.resultsPrev, self.cur_result > 0) 240 | mqt.SetWidgetEnabled(self.resultsNext, self.cur_result < len(self.results) - 1) 241 | 242 | mqt.SetWidgetEnabled(self.showDetails, step.has_details()) 243 | 244 | text = self.format_step_text(self.cur_result) 245 | 246 | if self.ctx.GetAction(self.eid): 247 | text = "Analysis step {} of {}:\n\n{}".format(self.cur_result + 1, len(self.results), text) 248 | 249 | mqt.SetWidgetVisible(self.texOutWidget, False) 250 | mqt.SetWidgetVisible(self.meshOutWidget, False) 251 | mqt.SetWidgetVisible(self.resultsSpacer, True) 252 | 253 | display = False 254 | if step.tex_display.resourceId != rd.ResourceId.Null(): 255 | display = True 256 | 257 | self.ctx.Replay().AsyncInvoke('', lambda _: self.texOut.SetTextureDisplay(step.tex_display)) 258 | 259 | mqt.SetWidgetVisible(self.texOutWidget, True) 260 | mqt.SetWidgetVisible(self.resultsSpacer, False) 261 | 262 | if display and self.eid != self.ctx.CurEvent(): 263 | mqt.SetWidgetVisible(self.texOutWidget, False) 264 | mqt.SetWidgetVisible(self.meshOutWidget, False) 265 | mqt.SetWidgetVisible(self.resultsSpacer, True) 266 | 267 | selected_eid = self.ctx.CurEvent() 268 | selected_draw = self.ctx.GetAction(selected_eid) 269 | 270 | text += '\n\n' 271 | text += 'Can\'t display visualisation for this step while another event {}: {} is selected' \ 272 | .format(selected_eid, self.get_action_name(selected_draw)) 273 | 274 | mqt.SetWidgetText(self.stepText, text) 275 | 276 | def format_step_text(self, step_index: int): 277 | step = self.results[step_index] 278 | 279 | text = step.msg 280 | 281 | if step.pixel_history.id != rd.ResourceId(): 282 | text += '\n\n' 283 | text += 'Full pixel history results at {},{} on {}:\n\n'.format(step.pixel_history.x, step.pixel_history.y, 284 | step.pixel_history.id) 285 | 286 | # filter the history only to the event in question, and the last prior passing event. 287 | history = [h for h in step.pixel_history.history if 288 | h.eventId == self.eid or h.eventId == step.pixel_history.last_eid] 289 | 290 | # remove any failing fragments from the previous draw 291 | history = [h for h in history if h.Passed() or h.eventId == self.eid] 292 | 293 | # remove all but the last fragment from the previous draw 294 | while len(history) > 2 and history[0].eventId == history[1].eventId == step.pixel_history.last_eid: 295 | del history[0] 296 | 297 | prev_eid = 0 298 | 299 | for h in history: 300 | d = self.ctx.GetAction(h.eventId) 301 | 302 | if d is None: 303 | name = '???' 304 | else: 305 | name = self.get_action_name(d) 306 | 307 | if prev_eid != h.eventId: 308 | text += "* @{}: {}\n".format(h.eventId, name) 309 | prev_eid = h.eventId 310 | 311 | prim = 'Unknown primitive' 312 | if h.primitiveID != 0xffffffff: 313 | prim = 'Primitive {}'.format(h.primitiveID) 314 | text += ' - {}:\n'.format(prim) 315 | text += ' Before: {}'.format(format_mod(h.preMod)) 316 | text += ' Fragment: Depth: {:.4}\n'.format(h.shaderOut.depth) 317 | if h.sampleMasked: 318 | text += ' The sample mask did not include this sample.\n' 319 | elif h.backfaceCulled: 320 | text += ' The primitive was backface culled.\n' 321 | elif h.depthClipped: 322 | text += ' The fragment was clipped by near/far plane.\n' 323 | elif h.depthBoundsFailed: 324 | text += ' The fragment was clipped by the depth bounds.\n' 325 | elif h.scissorClipped: 326 | text += ' The fragment was clipped by the scissor region.\n' 327 | elif h.shaderDiscarded: 328 | text += ' The pixel shader discarded this fragment.\n' 329 | elif h.depthTestFailed: 330 | text += ' The fragment failed the depth test outputting.\n' 331 | elif h.stencilTestFailed: 332 | text += ' The fragment failed the stencil test.\n' 333 | text += ' After: {}'.format(format_mod(h.postMod)) 334 | 335 | return text 336 | 337 | def goto_details(self): 338 | step: analyse.ResultStep = self.results[self.cur_result] 339 | 340 | if step.pipe_stage != qrd.PipelineStage.ComputeShader: 341 | self.ctx.ShowPipelineViewer() 342 | panel = self.ctx.GetPipelineViewer() 343 | panel.SelectPipelineStage(step.pipe_stage) 344 | 345 | self.ctx.RaiseDockWindow(panel.Widget()) 346 | return 347 | 348 | if step.tex_display.resourceId != rd.ResourceId.Null(): 349 | self.ctx.ShowTextureViewer() 350 | panel = self.ctx.GetTextureViewer() 351 | panel.ViewTexture(step.tex_display.resourceId, step.tex_display.typeCast, True) 352 | panel.SetSelectedSubresource(step.tex_display.subresource) 353 | panel.SetTextureOverlay(step.tex_display.overlay) 354 | panel.SetZoomLevel(True, 1.0) 355 | 356 | self.ctx.RaiseDockWindow(panel.Widget()) 357 | return 358 | 359 | if step.mesh_view != rd.MeshDataStage.Count: 360 | self.ctx.ShowMeshPreview() 361 | panel = self.ctx.GetMeshPreview() 362 | panel.ScrollToRow(0, step.mesh_view) 363 | 364 | panel.SetPreviewStage(step.mesh_view) 365 | 366 | self.ctx.RaiseDockWindow(panel.Widget()) 367 | return 368 | 369 | if step.pixel_history.id != rd.ResourceId(): 370 | panel = self.ctx.ViewPixelHistory(step.pixel_history.id, step.pixel_history.x, step.pixel_history.y, 371 | step.pixel_history.view, step.pixel_history.tex_display) 372 | panel.SetHistory(step.pixel_history.history) 373 | 374 | self.ctx.AddDockWindow(panel.Widget(), qrd.DockReference.AddTo, self.topWindow) 375 | return 376 | 377 | 378 | cur_window: Optional[Window] = None 379 | 380 | 381 | def closed(): 382 | global cur_window 383 | if cur_window is not None: 384 | cur_window.closed() 385 | cur_window.ctx.RemoveCaptureViewer(cur_window) 386 | cur_window = None 387 | 388 | 389 | def get_window(ctx, version): 390 | global cur_window, mqt 391 | 392 | mqt = ctx.Extensions().GetMiniQtHelper() 393 | 394 | if cur_window is None: 395 | cur_window = Window(ctx, version) 396 | 397 | return cur_window.topWindow 398 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | I want RenderDoc to be an open and welcoming project, and for that reason I want to make sure that people feel secure joining the project by outlining and enforcing expected behaviour from members of the community. This code of conduct is used in many open source projects and embodies that spirit of inclusiveness well. 2 | 3 | The full text is listed below. Anywhere that 'community leaders' are referred to as a collective group, this currently refers just directly to me. If you have any queries or issues with the code of conduct, you can get in touch with me [directly via email](mailto:baldurk@baldurk.org) 4 | 5 | # Contributor Covenant Code of Conduct 6 | 7 | ## Our Pledge 8 | 9 | We as members, contributors, and leaders pledge to make participation in our 10 | community a harassment-free experience for everyone, regardless of age, body 11 | size, visible or invisible disability, ethnicity, sex characteristics, gender 12 | identity and expression, level of experience, education, socio-economic status, 13 | nationality, personal appearance, race, religion, or sexual identity 14 | and orientation. 15 | 16 | We pledge to act and interact in ways that contribute to an open, welcoming, 17 | diverse, inclusive, and healthy community. 18 | 19 | ## Our Standards 20 | 21 | Examples of behavior that contributes to a positive environment for our 22 | community include: 23 | 24 | * Demonstrating empathy and kindness toward other people 25 | * Being respectful of differing opinions, viewpoints, and experiences 26 | * Giving and gracefully accepting constructive feedback 27 | * Accepting responsibility and apologizing to those affected by our mistakes, 28 | and learning from the experience 29 | * Focusing on what is best not just for us as individuals, but for the 30 | overall community 31 | 32 | Examples of unacceptable behavior include: 33 | 34 | * The use of sexualized language or imagery, and sexual attention or 35 | advances of any kind 36 | * Trolling, insulting or derogatory comments, and personal or political attacks 37 | * Public or private harassment 38 | * Publishing others' private information, such as a physical or email 39 | address, without their explicit permission 40 | * Other conduct which could reasonably be considered inappropriate in a 41 | professional setting 42 | 43 | ## Enforcement Responsibilities 44 | 45 | Community leaders are responsible for clarifying and enforcing our standards of 46 | acceptable behavior and will take appropriate and fair corrective action in 47 | response to any behavior that they deem inappropriate, threatening, offensive, 48 | or harmful. 49 | 50 | Community leaders have the right and responsibility to remove, edit, or reject 51 | comments, commits, code, wiki edits, issues, and other contributions that are 52 | not aligned to this Code of Conduct, and will communicate reasons for moderation 53 | decisions when appropriate. 54 | 55 | ## Scope 56 | 57 | This Code of Conduct applies within all community spaces, and also applies when 58 | an individual is officially representing the community in public spaces. 59 | Examples of representing our community include using an official e-mail address, 60 | posting via an official social media account, or acting as an appointed 61 | representative at an online or offline event. 62 | 63 | ## Enforcement 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 66 | reported to the community leaders responsible for enforcement at 67 | [baldurk@baldurk.org](mailto:baldurk@baldurk.org). 68 | All complaints will be reviewed and investigated promptly and fairly. 69 | 70 | All community leaders are obligated to respect the privacy and security of the 71 | reporter of any incident. 72 | 73 | ## Enforcement Guidelines 74 | 75 | Community leaders will follow these Community Impact Guidelines in determining 76 | the consequences for any action they deem in violation of this Code of Conduct: 77 | 78 | ### 1. Correction 79 | 80 | **Community Impact**: Use of inappropriate language or other behavior deemed 81 | unprofessional or unwelcome in the community. 82 | 83 | **Consequence**: A private, written warning from community leaders, providing 84 | clarity around the nature of the violation and an explanation of why the 85 | behavior was inappropriate. A public apology may be requested. 86 | 87 | ### 2. Warning 88 | 89 | **Community Impact**: A violation through a single incident or series 90 | of actions. 91 | 92 | **Consequence**: A warning with consequences for continued behavior. No 93 | interaction with the people involved, including unsolicited interaction with 94 | those enforcing the Code of Conduct, for a specified period of time. This 95 | includes avoiding interactions in community spaces as well as external channels 96 | like social media. Violating these terms may lead to a temporary or 97 | permanent ban. 98 | 99 | ### 3. Temporary Ban 100 | 101 | **Community Impact**: A serious violation of community standards, including 102 | sustained inappropriate behavior. 103 | 104 | **Consequence**: A temporary ban from any sort of interaction or public 105 | communication with the community for a specified period of time. No public or 106 | private interaction with the people involved, including unsolicited interaction 107 | with those enforcing the Code of Conduct, is allowed during this period. 108 | Violating these terms may lead to a permanent ban. 109 | 110 | ### 4. Permanent Ban 111 | 112 | **Community Impact**: Demonstrating a pattern of violation of community 113 | standards, including sustained inappropriate behavior, harassment of an 114 | individual, or aggression toward or disparagement of classes of individuals. 115 | 116 | **Consequence**: A permanent ban from any sort of public interaction within 117 | the community. 118 | 119 | ## Attribution 120 | 121 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 122 | version 2.0, available at 123 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 124 | 125 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 126 | enforcement ladder](https://github.com/mozilla/diversity). 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | 130 | For answers to common questions about this code of conduct, see the FAQ at 131 | https://www.contributor-covenant.org/faq. Translations are available at 132 | https://www.contributor-covenant.org/translations. 133 | 134 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | This repository is intended to be community driven, and as so contributing is made as simple as possible. No coding style or commit formatting requirements are applied to community-supplied extensions, it is up to the owner of each extension to decide. Issues can be filed against any extension but it's up to the author/owner of that extension to respond to issues or approve pull requests on their extensions. 4 | 5 | ## Code of Conduct 6 | 7 | I want to ensure that anyone can be free to contribute and feel welcome in the community. For that reason the project has adopted the same [contributor covenent](CODE_OF_CONDUCT.md) as RenderDoc as a code of conduct to be enforced for anyone involved in this repository. This includes any comments on issues or any public discussion in related places such as any RenderDoc spaces - e.g. the #renderdoc IRC channel or discord server. 8 | 9 | If you have any queries or concerns in this regard you can get in touch with me [directly over email](mailto:baldurk@baldurk.org). 10 | 11 | ## Acceptable extensions 12 | 13 | RenderDoc is a tool intended for debugging your own projects and programs, those to which you have true ownership of. Use and abuse of RenderDoc for illegal or unethical uses including but not limited to capturing copyrighted programs that you do not own the rights to will not be tolerated. This means any extensions related to exporting data, or capturing programs will not be accepted. 14 | 15 | ## Copyright / Contributor License Agreement 16 | 17 | Any code you submit will remain copyright to yourself, and it's recommended that you have a copyright header on your source files or a LICENSE file in your subdirectory clarifying your copyright ownership. For simplicity of management and to foster an open community your code must be licensed under the [MIT license](../LICENSE.md). You can of course still write extensions that are not licensed this way, but they would not be eligible for inclusion in this repository. 18 | 19 | -------------------------------------------------------------------------------- /example/empty-extension/README.md: -------------------------------------------------------------------------------- 1 | # Empty extension 2 | 3 | This extension is a completely empty example showing the bare bones of what is needed to register an extension with RenderDoc and get a callback to run custom code. 4 | 5 | For more information see [How do I register a python extension?](https://renderdoc.org/docs/how/how_python_extension.html) on the process of registering this extension with the UI, and [Writing UI extensions](https://renderdoc.org/docs/python_api/ui_extensions.html) for information on how to get started accessing the python API. 6 | -------------------------------------------------------------------------------- /example/empty-extension/__init__.py: -------------------------------------------------------------------------------- 1 | import qrenderdoc as qrd 2 | 3 | # This extension is a completely empty example showing the bare bones of what 4 | # is needed to register an extension with RenderDoc and get a callback to run 5 | # custom code. 6 | 7 | # For more information see: 8 | # https://renderdoc.org/docs/how/how_python_extension.html 9 | # 10 | # for the process of registering this extension with the UI, and: 11 | # https://renderdoc.org/docs/python_api/ui_extensions.html 12 | # 13 | # for information on how to get started accessing the python API. 14 | 15 | def register(version: str, ctx: qrd.CaptureContext): 16 | print("Registering empty extension") 17 | 18 | def unregister(): 19 | print("Unregistering empty extension") 20 | -------------------------------------------------------------------------------- /example/empty-extension/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_api": 1, 3 | "name": "Example empty UI extension", 4 | "version": "1.0", 5 | "minimum_renderdoc": "1.2", 6 | "description": "This is a UI extension which does nothing, and serves only as a template.", 7 | "author": "Baldur Karlsson ", 8 | "url": "https://github.com/baldurk/renderdoc-contrib" 9 | } 10 | --------------------------------------------------------------------------------