├── LICENSE ├── README.md └── ShaderCrashingAssert.h /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adam Sawicki 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ShaderCrashingAssert is a small library for programmers who use C++, Windows, and Direct3D 12. It provides an assert-like macro for HLSL shader language that causes memory page fault. Together with GPU crash analysis tools like **[Radeon GPU Detective](https://gpuopen.com/radeon-gpu-detective/)** for AMD cards it can provide help in shader debugging. 4 | 5 | Author: Adam Sawicki - http://asawicki.info
6 | Version: 0.0.1, 2023-08-19
7 | License: MIT 8 | 9 | # User guide 10 | 11 | This is a single-header library, so you only need to copy file "ShaderCrashingAssert.h" into your project. It is truly header-only library, not "STB-style", which means all the functions are defined as inline and there is no need to extract the implementation into a .cpp file with some macro. 12 | 13 | ## Integration in C++ code 14 | 15 | 1. Include the library in your C++ code: 16 | 17 | ``` 18 | #include "ShaderCrashingAssert.h" 19 | ``` 20 | 21 | Direct3D 12 `` must be included earlier. 22 | 23 | 2. Create and initialize the main context object. The object should be created after `ID3D12Device` and destroyed before the device is destroyed. 24 | 25 | ``` 26 | ID3D12Device* device = ... 27 | 28 | SHADER_CRASHING_ASSERT_CONTEXT_DESC ctxDesc = {}; 29 | ctxDesc.pDevice = device; 30 | ShaderCrashingAssertContext* ctx = new ShaderCrashingAssertContext(); 31 | HRESULT hr = ctx->Init(&ctxDesc); 32 | if(FAILED(hr)) ... // Handle error 33 | 34 | // YOUR APPLICATION WORKING HERE 35 | 36 | delete ctx; 37 | device->Release(); 38 | ``` 39 | 40 | 3. Prepare a UAV descriptor. This library needs a special descriptor to work. Context object provides a CPU handle to that descriptor in a non-shader-visible descriptor heap, which you need to copy into your shader-visible descriptor heap that you use for rendering. 41 | 42 | ``` 43 | device->CopyDescriptorsSimple( 44 | 1, // NumDescriptors 45 | myDescriptorHeap->GetCPUHandle(), // DestDescriptorRangeStart 46 | ctx->GetUAVCPUDescriptorHandle(), // SrcDescriptorRangeStart 47 | D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); // DescriptorHeapsType 48 | ``` 49 | 50 | 4. In your root signature, declare a UAV where this descriptor will be passed. It can be located at any register slot and space of your choice (for example: slot u6 in space7). 51 | 52 | 5. Before issuing a draw call or a compute dispatch that uses the assert macro, you need to set this descriptor at the correct root parameter index: 53 | 54 | ``` 55 | cmdList->SetGraphicsRootDescriptorTable(rootParameterIndex, myDescriptorHeap->GetGPUHandle()); 56 | ``` 57 | 58 | If the resource is not bound, the shader assert doesn't trigger a crash. 59 | 60 | *If these steps required for resource binding in D3D12 look too complicated for you, this article can help understand them: [Direct3D 12: Long Way to Access Data](https://asawicki.info/news_1754_direct3d_12_long_way_to_access_data).* 61 | 62 | ## Integration in HLSL shader code 63 | 64 | 1. Include the library: 65 | 66 | ``` 67 | #include "ShaderCrashingAssert.h" 68 | ``` 69 | 70 | Yes, this is not a mistake. The file automatically uses CPU/C++ or GPU/HLSL part depending on predefined macros. 71 | 72 | 2. Declare the UAV resource needed by the library using provided macro. You need to specify register number and space matching those declared in the root signature. 73 | 74 | ``` 75 | SHADER_CRASHING_ASSERT_RESOURCE : register(u6, space7); 76 | ``` 77 | 78 | 3. Finally, in the HLSL function where you want to check some critical condition, use the macro `SHADER_CRASHING_ASSERT(expr)`. Argument should be boolean with true value when everything is OK. When false, a memory page fault is triggered. 79 | 80 | ``` 81 | float3 color = ... 82 | SHADER_CRASHING_ASSERT(!any(isnan(color))); 83 | ``` 84 | 85 | ## Usage 86 | 87 | A GPU memory page fault generally results in an undefined behavior. The application may observe an error like `DXGI_ERROR_DEVICE_REMOVED`, `DXGI_ERROR_DEVICE_HUNG`, `DXGI_ERROR_DEVICE_RESET` returned from one of the D3D12/DXGI functions in the same rendering frame, in some future frame, or it may continue normally. 88 | 89 | However, with **[Radeon GPU Detective](https://gpuopen.com/radeon-gpu-detective/)** active (Radeon Developer Panel launched and set to "Crash Analysis" mode), such crash seems to be captured more reliably. Application still observes D3D12 error, but RGD can then show information about the render pass and draw call or dispatch that triggered it. To make sure it was the assert from this library and not some other GPU failure, look for resources named "ShaderCrashingAssert" in the RGD output. For more information, check [RGD tutorial](https://gpuopen.com/learn/rgd_1_0_tutorial/) or documentation of the tool: [Quickstart Guide](https://radeon-gpu-detective.readthedocs.io/en/latest/quickstart.html) and [Help Manual](https://radeon-gpu-detective.readthedocs.io/en/latest/help_manual.html). 90 | 91 | **Please remember that this whole library is a hack and may not be fully reliable. On some systems, GPUs, with some applications, crash may not happen despite the assert is triggered. Please always test asserting unconditionally before using this library for debugging.** 92 | 93 | One possible explanation is that because the library creates and destroys a small buffer, then the application creates some more resources, they may have the same address assigned, so the address is not invalid. If this is the case, possibly moving `Init()` call later in the application initialization code can help. 94 | 95 | # Technical considerations 96 | 97 | ## How does it work? 98 | 99 | The library creates a small UAV buffer, a raw UAV descriptor for it, declares a `RWByteAddressBuffer` resource in shader code, and performs a `Store()` to it in the main assert macro. The buffer and its heap is released soon after creation, so the descriptor points to an incorrect address, which triggers the page fault. 100 | 101 | The need to use a UAV resource is an inconvenience, but it is necessary because shaders don't have a free-form pointers to be able to just reach out to some address. 102 | 103 | D3D Debug Layer doesn't report this logic as an error because it doesn't track the content of the descriptors. GPU-based validation may be able to find it - I didn't check. 104 | 105 | ## Perspectives for returning extra data 106 | 107 | It would be nice to be able to return additional data when an assert is hit. For example, some `SHADER_CRASHING_ASSERT2(uint val)` could be defined that crashes whenever argument is non-zero and returns that value somehow. 108 | 109 | 1. One possible idea is to encode this value in the offending virtual address (VA) of the crash. Unfortunately, I couldn't make it working. 110 | 111 | 1. Possibly least significant bits of the address could be controlled by performing the UAV `Store()` to a specific offsets in the buffer instead of zero. Unfortunately, it doesn't work - when tested, RGD still returns an address like 0x2008f9000. The address is likely aligned down to 0x1000 = 4 KB. 112 | 113 | 2. Possibly, higher bits could be used if we know that the beginning of the buffer is aligned to some large number. Theoretically, a `ID3D12Heap` can be created with specific alignment, notably `D3D12_DEFAULT_MSAA_RESOURCE_PLACEMENT_ALIGNMENT`, equal to 4194304 = 4 MB, which is required when placing MSAA textures inside. Unfortunately, it doesn't work. Even when using this alignment, also with `D3D12_HEAP_FLAG_NONE` instead of `D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS`, RGD still shows an address like 0x2008f9000, so the heap is still aligned only to 4K. 114 | 115 | 2. Other potential solution is to simply create several buffers and choose between them when triggering the assert. They could be then distinguished by their string names or different sizes in the RGD report. If you are interested in having this feature, please let me know by creating an Issue ticket in this repository. 116 | 117 | # Final words 118 | 119 | Thanks to Manon Oomen from Traverse Research for the inspiration! 120 | -------------------------------------------------------------------------------- /ShaderCrashingAssert.h: -------------------------------------------------------------------------------- 1 | /* 2 | ShaderCrashingAssert: Small library for D3D12. Provides assert-like macro for HLSL that crashes the GPU. 3 | 4 | Author: Adam Sawicki - http://asawicki.info - adam__DELETE__@asawicki.info 5 | Version: 0.0.1, 2023-08-19 6 | License: MIT 7 | 8 | Documentation: see README.md in the repository: https://github.com/sawickiap/ShaderCrashingAssert 9 | 10 | # Version history 11 | 12 | Version 0.0.1, 2023-08-19 13 | 14 | First version. 15 | 16 | # License 17 | 18 | Copyright (c) 2023 Adam Sawicki 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | */ 38 | #ifndef SHADER_CRASHING_ASSERT_H_ 39 | #define SHADER_CRASHING_ASSERT_H_ 40 | 41 | //////////////////////////////////////////////////////////////////////////////// 42 | // C++ section 43 | #ifdef __cplusplus 44 | 45 | #ifndef __d3d12_h__ 46 | #error Please #include before including "ShaderCrashingAssert.h" 47 | #endif 48 | 49 | struct SHADER_CRASHING_ASSERT_CONTEXT_DESC 50 | { 51 | ID3D12Device* pDevice; 52 | }; 53 | 54 | class ShaderCrashingAssertContext 55 | { 56 | public: 57 | HRESULT Init(const SHADER_CRASHING_ASSERT_CONTEXT_DESC* pDesc) 58 | { 59 | constexpr UINT64 size = 32; 60 | HRESULT hr = S_OK; 61 | 62 | // Create heap 63 | ID3D12Heap* heap = nullptr; 64 | { 65 | D3D12_HEAP_DESC heapDesc = {}; 66 | heapDesc.SizeInBytes = size; 67 | heapDesc.Properties.Type = D3D12_HEAP_TYPE_DEFAULT; 68 | heapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS; 69 | hr = pDesc->pDevice->CreateHeap(&heapDesc, IID_PPV_ARGS(&heap)); 70 | } 71 | if (SUCCEEDED(hr)) 72 | { 73 | heap->SetName(L"ShaderCrashingAssert Heap"); 74 | } 75 | 76 | // Create buffer 77 | ID3D12Resource* buf = nullptr; 78 | if (SUCCEEDED(hr)) 79 | { 80 | D3D12_RESOURCE_DESC resDesc = {}; 81 | resDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; 82 | resDesc.Width = size; 83 | resDesc.Height = 1; 84 | resDesc.DepthOrArraySize = 1; 85 | resDesc.MipLevels = 1; 86 | resDesc.SampleDesc.Count = 1; 87 | resDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; 88 | resDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; 89 | hr = pDesc->pDevice->CreatePlacedResource(heap, 0, &resDesc, D3D12_RESOURCE_STATE_COMMON, nullptr, 90 | IID_PPV_ARGS(&buf)); 91 | } 92 | if (SUCCEEDED(hr)) 93 | { 94 | buf->SetName(L"ShaderCrashingAssert Buffer"); 95 | } 96 | 97 | // Create descriptor heap 98 | if (SUCCEEDED(hr)) 99 | { 100 | D3D12_DESCRIPTOR_HEAP_DESC descHeapDesc = {}; 101 | descHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; 102 | descHeapDesc.NumDescriptors = 1; 103 | hr = pDesc->pDevice->CreateDescriptorHeap(&descHeapDesc, IID_PPV_ARGS(&m_pDescriptorHeap)); 104 | } 105 | if (SUCCEEDED(hr)) 106 | { 107 | m_pDescriptorHeap->SetName(L"ShaderCrashingAssert Descriptor Heap"); 108 | } 109 | 110 | // Setup the descriptor 111 | if (SUCCEEDED(hr)) 112 | { 113 | D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; 114 | uavDesc.Format = DXGI_FORMAT_R32_TYPELESS; 115 | uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER; 116 | uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW; 117 | uavDesc.Buffer.NumElements = size / sizeof(DWORD); 118 | pDesc->pDevice->CreateUnorderedAccessView(buf, NULL, &uavDesc, 119 | m_pDescriptorHeap->GetCPUDescriptorHandleForHeapStart()); 120 | } 121 | 122 | // Release buffer and heap 123 | if (buf) 124 | { 125 | buf->Release(); 126 | } 127 | if (heap) 128 | { 129 | heap->Release(); 130 | } 131 | 132 | return hr; 133 | } 134 | 135 | ~ShaderCrashingAssertContext() 136 | { 137 | if (m_pDescriptorHeap) 138 | { 139 | m_pDescriptorHeap->Release(); 140 | } 141 | } 142 | 143 | D3D12_CPU_DESCRIPTOR_HANDLE GetUAVCPUDescriptorHandle() 144 | { 145 | return m_pDescriptorHeap->GetCPUDescriptorHandleForHeapStart(); 146 | } 147 | 148 | private: 149 | ID3D12DescriptorHeap* m_pDescriptorHeap = nullptr; 150 | }; 151 | 152 | //////////////////////////////////////////////////////////////////////////////// 153 | // HLSL section 154 | #else // #ifdef __cplusplus 155 | 156 | #define SHADER_CRASHING_ASSERT_RESOURCE \ 157 | globallycoherent RWByteAddressBuffer ShaderCrashingAssertResource1 158 | 159 | #define SHADER_CRASHING_ASSERT(expr) \ 160 | do { \ 161 | [branch] if (!(expr)) \ 162 | { \ 163 | ShaderCrashingAssertResource1.Store( \ 164 | 0, \ 165 | 0x23898f4a); \ 166 | } \ 167 | } while(false) 168 | // That specific numerical value doesn't matter. 169 | 170 | #endif // #ifdef __cplusplus 171 | 172 | #endif // #ifndef SHADER_CRASHING_ASSERT_H_ 173 | --------------------------------------------------------------------------------