1# 2# Copyright (c) 2018-2023 Advanced Micro Devices, Inc. All rights reserved. 3# 4# Permission is hereby granted, free of charge, to any person obtaining a copy 5# of this software and associated documentation files (the "Software"), to deal 6# in the Software without restriction, including without limitation the rights 7# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8# copies of the Software, and to permit persons to whom the Software is 9# furnished to do so, subject to the following conditions: 10# 11# The above copyright notice and this permission notice shall be included in 12# all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20# THE SOFTWARE. 21# 22 23import argparse 24import json 25from PIL import Image, ImageDraw, ImageFont 26 27 28PROGRAM_VERSION = 'Vulkan/D3D12 Memory Allocator Dump Visualization 3.0.2' 29IMG_WIDTH = 1200 30IMG_MARGIN = 8 31TEXT_MARGIN = 4 32FONT_SIZE = 10 33MAP_SIZE = 24 34COLOR_TEXT_H1 = (0, 0, 0, 255) 35COLOR_TEXT_H2 = (150, 150, 150, 255) 36COLOR_OUTLINE = (155, 155, 155, 255) 37COLOR_OUTLINE_HARD = (0, 0, 0, 255) 38COLOR_GRID_LINE = (224, 224, 224, 255) 39 40currentApi = "" 41data = {} 42 43 44def ParseArgs(): 45 argParser = argparse.ArgumentParser(description='Visualization of Vulkan/D3D12 Memory Allocator JSON dump.') 46 argParser.add_argument('DumpFile', help='Path to source JSON file with memory dump created by Vulkan/D3D12 Memory Allocator library') 47 argParser.add_argument('-v', '--version', action='version', version=PROGRAM_VERSION) 48 argParser.add_argument('-o', '--output', required=True, help='Path to destination image file (e.g. PNG)') 49 return argParser.parse_args() 50 51def GetDataForMemoryPool(poolTypeName): 52 global data 53 if poolTypeName in data: 54 return data[poolTypeName] 55 else: 56 newPoolData = {'DedicatedAllocations':[], 'Blocks':[], 'CustomPools':{}} 57 data[poolTypeName] = newPoolData 58 return newPoolData 59 60def ProcessBlock(poolData, block): 61 blockInfo = {'ID': block[0], 'Size': int(block[1]['TotalBytes']), 'Suballocations':[]} 62 for alloc in block[1]['Suballocations']: 63 allocData = {'Type': alloc['Type'], 'Size': int(alloc['Size']), 'Usage': int(alloc['Usage']) if 'Usage' in alloc else 0 } 64 blockInfo['Suballocations'].append(allocData) 65 poolData['Blocks'].append(blockInfo) 66 67def IsDataEmpty(): 68 global data 69 for poolData in data.values(): 70 if len(poolData['DedicatedAllocations']) > 0: 71 return False 72 if len(poolData['Blocks']) > 0: 73 return False 74 for customPool in poolData['CustomPools'].values(): 75 if len(customPool['Blocks']) > 0: 76 return False 77 if len(customPool['DedicatedAllocations']) > 0: 78 return False 79 return True 80 81def RemoveEmptyType(): 82 global data 83 for poolType in list(data.keys()): 84 pool = data[poolType] 85 if len(pool['DedicatedAllocations']) > 0: 86 continue 87 if len(pool['Blocks']) > 0: 88 continue 89 empty = True 90 for customPool in pool['CustomPools'].values(): 91 if len(customPool['Blocks']) > 0: 92 empty = False 93 break 94 if len(customPool['DedicatedAllocations']) > 0: 95 empty = False 96 break 97 if empty: 98 del data[poolType] 99 100# Returns tuple: 101# [0] image height : integer 102# [1] pixels per byte : float 103def CalcParams(): 104 global data 105 height = IMG_MARGIN 106 height += FONT_SIZE + IMG_MARGIN # Grid lines legend - sizes 107 maxBlockSize = 0 108 # Get height occupied by every memory pool 109 for poolData in data.values(): 110 height += FONT_SIZE + IMG_MARGIN # Memory pool title 111 height += len(poolData['Blocks']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2) 112 height += len(poolData['DedicatedAllocations']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2) 113 # Get longest block size 114 for dedicatedAlloc in poolData['DedicatedAllocations']: 115 maxBlockSize = max(maxBlockSize, dedicatedAlloc['Size']) 116 for block in poolData['Blocks']: 117 maxBlockSize = max(maxBlockSize, block['Size']) 118 # Same for custom pools 119 for customPoolData in poolData['CustomPools'].values(): 120 height += len(customPoolData['Blocks']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2) 121 height += len(customPoolData['DedicatedAllocations']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2) 122 # Get longest block size 123 for dedicatedAlloc in customPoolData['DedicatedAllocations']: 124 maxBlockSize = max(maxBlockSize, dedicatedAlloc['Size']) 125 for block in customPoolData['Blocks']: 126 maxBlockSize = max(maxBlockSize, block['Size']) 127 128 return height, (IMG_WIDTH - IMG_MARGIN * 2) / float(maxBlockSize) 129 130def BytesToStr(bytes): 131 if bytes < 1024: 132 return "%d B" % bytes 133 bytes /= 1024 134 if bytes < 1024: 135 return "%d KiB" % bytes 136 bytes /= 1024 137 if bytes < 1024: 138 return "%d MiB" % bytes 139 bytes /= 1024 140 return "%d GiB" % bytes 141 142def TypeToColor(type, usage): 143 global currentApi 144 if type == 'FREE': 145 return 220, 220, 220, 255 146 elif type == 'UNKNOWN': 147 return 175, 175, 175, 255 # Gray 148 149 if currentApi == 'Vulkan': 150 if type == 'BUFFER': 151 if (usage & 0x1C0) != 0: # INDIRECT_BUFFER | VERTEX_BUFFER | INDEX_BUFFER 152 return 255, 148, 148, 255 # Red 153 elif (usage & 0x28) != 0: # STORAGE_BUFFER | STORAGE_TEXEL_BUFFER 154 return 255, 187, 121, 255 # Orange 155 elif (usage & 0x14) != 0: # UNIFORM_BUFFER | UNIFORM_TEXEL_BUFFER 156 return 255, 255, 0, 255 # Yellow 157 else: 158 return 255, 255, 165, 255 # Light yellow 159 elif type == 'IMAGE_OPTIMAL': 160 if (usage & 0x20) != 0: # DEPTH_STENCIL_ATTACHMENT 161 return 246, 128, 255, 255 # Pink 162 elif (usage & 0xD0) != 0: # INPUT_ATTACHMENT | TRANSIENT_ATTACHMENT | COLOR_ATTACHMENT 163 return 179, 179, 255, 255 # Blue 164 elif (usage & 0x4) != 0: # SAMPLED 165 return 0, 255, 255, 255 # Aqua 166 else: 167 return 183, 255, 255, 255 # Light aqua 168 elif type == 'IMAGE_LINEAR' : 169 return 0, 255, 0, 255 # Green 170 elif type == 'IMAGE_UNKNOWN': 171 return 0, 255, 164, 255 # Green/aqua 172 elif currentApi == 'Direct3D 12': 173 if type == 'BUFFER': 174 return 255, 255, 165, 255 # Light yellow 175 elif type == 'TEXTURE1D' or type == 'TEXTURE2D' or type == 'TEXTURE3D': 176 if (usage & 0x2) != 0: # D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL 177 return 246, 128, 255, 255 # Pink 178 elif (usage & 0x5) != 0: # D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS 179 return 179, 179, 255, 255 # Blue 180 elif (usage & 0x8) == 0: # Not having D3D12_RESOURCE_FLAG_DENY_SHARED_RESOURCE 181 return 0, 255, 255, 255 # Aqua 182 else: 183 return 183, 255, 255, 255 # Light aqua 184 else: 185 print("Unknown graphics API!") 186 exit(1) 187 assert False 188 return 0, 0, 0, 255 189 190def DrawBlock(draw, y, block, pixelsPerByte): 191 sizePixels = int(block['Size'] * pixelsPerByte) 192 draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + sizePixels, y + MAP_SIZE], fill=TypeToColor('FREE', 0), outline=None) 193 byte = 0 194 x = 0 195 lastHardLineX = -1 196 for alloc in block['Suballocations']: 197 byteEnd = byte + alloc['Size'] 198 xEnd = int(byteEnd * pixelsPerByte) 199 if alloc['Type'] != 'FREE': 200 if xEnd > x + 1: 201 draw.rectangle([IMG_MARGIN + x, y, IMG_MARGIN + xEnd, y + MAP_SIZE], fill=TypeToColor(alloc['Type'], alloc['Usage']), outline=COLOR_OUTLINE) 202 # Hard line was been overwritten by rectangle outline: redraw it. 203 if lastHardLineX == x: 204 draw.line([IMG_MARGIN + x, y, IMG_MARGIN + x, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) 205 else: 206 draw.line([IMG_MARGIN + x, y, IMG_MARGIN + x, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) 207 lastHardLineX = x 208 byte = byteEnd 209 x = xEnd 210 211def DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte): 212 sizePixels = int(dedicatedAlloc['Size'] * pixelsPerByte) 213 draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + sizePixels, y + MAP_SIZE], fill=TypeToColor(dedicatedAlloc['Type'], dedicatedAlloc['Usage']), outline=COLOR_OUTLINE) 214 215 216if __name__ == '__main__': 217 args = ParseArgs() 218 jsonSrc = json.load(open(args.DumpFile, 'rb')) 219 220 if 'General' in jsonSrc: 221 currentApi = jsonSrc['General']['API'] 222 else: 223 print("Wrong JSON format, cannot determine graphics API!") 224 exit(1) 225 226 # Process default pools 227 if 'DefaultPools' in jsonSrc: 228 for memoryPool in jsonSrc['DefaultPools'].items(): 229 poolData = GetDataForMemoryPool(memoryPool[0]) 230 # Get dedicated allocations 231 for dedicatedAlloc in memoryPool[1]['DedicatedAllocations']: 232 allocData = {'Type': dedicatedAlloc['Type'], 'Size': int(dedicatedAlloc['Size']), 'Usage': int(dedicatedAlloc['Usage'])} 233 poolData['DedicatedAllocations'].append(allocData) 234 # Get allocations in block vectors 235 for block in memoryPool[1]['Blocks'].items(): 236 ProcessBlock(poolData, block) 237 # Process custom pools 238 if 'CustomPools' in jsonSrc: 239 for memoryPool in jsonSrc['CustomPools'].items(): 240 poolData = GetDataForMemoryPool(memoryPool[0]) 241 for pool in memoryPool[1]: 242 poolName = pool['Name'] 243 poolData['CustomPools'][poolName] = {'DedicatedAllocations':[], 'Blocks':[]} 244 # Get dedicated allocations 245 for dedicatedAlloc in pool['DedicatedAllocations']: 246 allocData = {'Type': dedicatedAlloc['Type'], 'Size': int(dedicatedAlloc['Size']), 'Usage': int(dedicatedAlloc['Usage'])} 247 poolData['CustomPools'][poolName]['DedicatedAllocations'].append(allocData) 248 # Get allocations in block vectors 249 for block in pool['Blocks'].items(): 250 ProcessBlock(poolData['CustomPools'][poolName], block) 251 252 if IsDataEmpty(): 253 print("There is nothing to put on the image. Please make sure you generated the stats string with detailed map enabled.") 254 exit(1) 255 RemoveEmptyType() 256 # Calculate dimmensions and create data image 257 imgHeight, pixelsPerByte = CalcParams() 258 img = Image.new('RGB', (IMG_WIDTH, imgHeight), 'white') 259 draw = ImageDraw.Draw(img) 260 try: 261 font = ImageFont.truetype('segoeuib.ttf') 262 except: 263 font = ImageFont.load_default() 264 265 # Draw grid lines 266 bytesBetweenGridLines = 32 267 while bytesBetweenGridLines * pixelsPerByte < 64: 268 bytesBetweenGridLines *= 2 269 byte = 0 270 y = IMG_MARGIN 271 while True: 272 x = int(byte * pixelsPerByte) 273 if x > IMG_WIDTH - 2 * IMG_MARGIN: 274 break 275 draw.line([x + IMG_MARGIN, 0, x + IMG_MARGIN, imgHeight], fill=COLOR_GRID_LINE) 276 if byte == 0: 277 draw.text((x + IMG_MARGIN + TEXT_MARGIN, y), "0", fill=COLOR_TEXT_H2, font=font) 278 else: 279 text = BytesToStr(byte) 280 textSize = draw.textsize(text, font=font) 281 draw.text((x + IMG_MARGIN - textSize[0] - TEXT_MARGIN, y), text, fill=COLOR_TEXT_H2, font=font) 282 byte += bytesBetweenGridLines 283 y += FONT_SIZE + IMG_MARGIN 284 285 # Draw main content 286 for memType in sorted(data.keys()): 287 memPoolData = data[memType] 288 draw.text((IMG_MARGIN, y), "Memory pool %s" % memType, fill=COLOR_TEXT_H1, font=font) 289 y += FONT_SIZE + IMG_MARGIN 290 # Draw block vectors 291 for block in memPoolData['Blocks']: 292 draw.text((IMG_MARGIN, y), "Default pool block %s" % block['ID'], fill=COLOR_TEXT_H2, font=font) 293 y += FONT_SIZE + IMG_MARGIN 294 DrawBlock(draw, y, block, pixelsPerByte) 295 y += MAP_SIZE + IMG_MARGIN 296 index = 0 297 # Draw dedicated allocations 298 for dedicatedAlloc in memPoolData['DedicatedAllocations']: 299 draw.text((IMG_MARGIN, y), "Dedicated allocation %d" % index, fill=COLOR_TEXT_H2, font=font) 300 y += FONT_SIZE + IMG_MARGIN 301 DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte) 302 y += MAP_SIZE + IMG_MARGIN 303 index += 1 304 for poolName, pool in memPoolData['CustomPools'].items(): 305 for block in pool['Blocks']: 306 draw.text((IMG_MARGIN, y), "Custom pool %s block %s" % (poolName, block['ID']), fill=COLOR_TEXT_H2, font=font) 307 y += FONT_SIZE + IMG_MARGIN 308 DrawBlock(draw, y, block, pixelsPerByte) 309 y += MAP_SIZE + IMG_MARGIN 310 index = 0 311 for dedicatedAlloc in pool['DedicatedAllocations']: 312 draw.text((IMG_MARGIN, y), "Custom pool %s dedicated allocation %d" % (poolName, index), fill=COLOR_TEXT_H2, font=font) 313 y += FONT_SIZE + IMG_MARGIN 314 DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte) 315 y += MAP_SIZE + IMG_MARGIN 316 index += 1 317 del draw 318 img.save(args.output) 319 320""" 321Main data structure - variable `data` - is a dictionary. Key is string - memory type name. Value is dictionary of: 322- Fixed key 'DedicatedAllocations'. Value is list of objects, each containing dictionary with: 323 - Fixed key 'Type'. Value is string. 324 - Fixed key 'Size'. Value is int. 325 - Fixed key 'Usage'. Value is int. 326- Fixed key 'Blocks'. Value is list of objects, each containing dictionary with: 327 - Fixed key 'ID'. Value is int. 328 - Fixed key 'Size'. Value is int. 329 - Fixed key 'Suballocations'. Value is list of objects as above. 330- Fixed key 'CustomPools'. Value is dictionary. 331 - Key is string with pool ID/name. Value is a dictionary with: 332 - Fixed key 'DedicatedAllocations'. Value is list of objects as above. 333 - Fixed key 'Blocks'. Value is a list of objects representing memory blocks as above. 334""" 335