# # Copyright (c) 2018-2022 Advanced Micro Devices, Inc. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import argparse import json from PIL import Image, ImageDraw, ImageFont PROGRAM_VERSION = 'Vulkan/D3D12 Memory Allocator Dump Visualization 3.0.0' IMG_WIDTH = 1200 IMG_MARGIN = 8 TEXT_MARGIN = 4 FONT_SIZE = 10 MAP_SIZE = 24 COLOR_TEXT_H1 = (0, 0, 0, 255) COLOR_TEXT_H2 = (150, 150, 150, 255) COLOR_OUTLINE = (155, 155, 155, 255) COLOR_OUTLINE_HARD = (0, 0, 0, 255) COLOR_GRID_LINE = (224, 224, 224, 255) currentApi = "" data = {} def ParseArgs(): argParser = argparse.ArgumentParser(description='Visualization of Vulkan/D3D12 Memory Allocator JSON dump.') argParser.add_argument('DumpFile', help='Path to source JSON file with memory dump created by Vulkan/D3D12 Memory Allocator library') argParser.add_argument('-v', '--version', action='version', version=PROGRAM_VERSION) argParser.add_argument('-o', '--output', required=True, help='Path to destination image file (e.g. PNG)') return argParser.parse_args() def GetDataForMemoryPool(poolTypeName): global data if poolTypeName in data: return data[poolTypeName] else: newPoolData = {'DedicatedAllocations':[], 'Blocks':[], 'CustomPools':{}} data[poolTypeName] = newPoolData return newPoolData def ProcessBlock(poolData, block): blockInfo = {'ID': block[0], 'Size': int(block[1]['TotalBytes']), 'Suballocations':[]} for alloc in block[1]['Suballocations']: allocData = {'Type': alloc['Type'], 'Size': int(alloc['Size']), 'Usage': int(alloc['Usage']) if 'Usage' in alloc else 0 } blockInfo['Suballocations'].append(allocData) poolData['Blocks'].append(blockInfo) def IsDataEmpty(): global data for poolData in data.values(): if len(poolData['DedicatedAllocations']) > 0: return False if len(poolData['Blocks']) > 0: return False for customPool in poolData['CustomPools'].values(): if len(customPool['Blocks']) > 0: return False if len(customPool['DedicatedAllocations']) > 0: return False return True def RemoveEmptyType(): global data for poolType in list(data.keys()): if len(data[poolType]['DedicatedAllocations']) > 0: continue if len(data[poolType]['Blocks']) > 0: continue empty = True for customPool in data[poolType]['CustomPools'].values(): if len(data[poolType]['Blocks']) > 0: empty = False break if len(data[poolType]['DedicatedAllocations']) > 0: empty = False break if empty: del data[poolType] # Returns tuple: # [0] image height : integer # [1] pixels per byte : float def CalcParams(): global data height = IMG_MARGIN height += FONT_SIZE + IMG_MARGIN # Grid lines legend - sizes maxBlockSize = 0 # Get height occupied by every memory pool for poolData in data.values(): height += IMG_MARGIN + FONT_SIZE height += len(poolData['DedicatedAllocations']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) height += len(poolData['Blocks']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) # Get longest block size for dedicatedAlloc in poolData['DedicatedAllocations']: maxBlockSize = max(maxBlockSize, dedicatedAlloc['Size']) for block in poolData['Blocks']: maxBlockSize = max(maxBlockSize, block['Size']) # Same for custom pools for customPoolData in poolData['CustomPools'].values(): height += len(customPoolData['DedicatedAllocations']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) height += len(customPoolData['Blocks']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) height += FONT_SIZE * 2 + IMG_MARGIN if len(customPoolData['DedicatedAllocations']) == 0 else 0 # Get longest block size for dedicatedAlloc in customPoolData['DedicatedAllocations']: maxBlockSize = max(maxBlockSize, dedicatedAlloc['Size']) for block in customPoolData['Blocks']: maxBlockSize = max(maxBlockSize, block['Size']) return height + FONT_SIZE, (IMG_WIDTH - IMG_MARGIN * 2) / float(maxBlockSize) def BytesToStr(bytes): if bytes < 1024: return "%d B" % bytes bytes /= 1024 if bytes < 1024: return "%d KiB" % bytes bytes /= 1024 if bytes < 1024: return "%d MiB" % bytes bytes /= 1024 return "%d GiB" % bytes def TypeToColor(type, usage): global currentApi if type == 'FREE': return 220, 220, 220, 255 elif type == 'UNKNOWN': return 175, 175, 175, 255 # Gray if currentApi == 'Vulkan': if type == 'BUFFER': if (usage & 0x1C0) != 0: # INDIRECT_BUFFER | VERTEX_BUFFER | INDEX_BUFFER return 255, 148, 148, 255 # Red elif (usage & 0x28) != 0: # STORAGE_BUFFER | STORAGE_TEXEL_BUFFER return 255, 187, 121, 255 # Orange elif (usage & 0x14) != 0: # UNIFORM_BUFFER | UNIFORM_TEXEL_BUFFER return 255, 255, 0, 255 # Yellow else: return 255, 255, 165, 255 # Light yellow elif type == 'IMAGE_OPTIMAL': if (usage & 0x20) != 0: # DEPTH_STENCIL_ATTACHMENT return 246, 128, 255, 255 # Pink elif (usage & 0xD0) != 0: # INPUT_ATTACHMENT | TRANSIENT_ATTACHMENT | COLOR_ATTACHMENT return 179, 179, 255, 255 # Blue elif (usage & 0x4) != 0: # SAMPLED return 0, 255, 255, 255 # Aqua else: return 183, 255, 255, 255 # Light aqua elif type == 'IMAGE_LINEAR' : return 0, 255, 0, 255 # Green elif type == 'IMAGE_UNKNOWN': return 0, 255, 164, 255 # Green/aqua elif currentApi == 'Direct3D 12': if type == 'BUFFER': return 255, 255, 165, 255 # Light yellow elif type == 'TEXTURE1D' or type == 'TEXTURE2D' or type == 'TEXTURE3D': if (usage & 0x2) != 0: # D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL return 246, 128, 255, 255 # Pink elif (usage & 0x5) != 0: # D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS return 179, 179, 255, 255 # Blue elif (usage & 0x8) == 0: # Not having D3D12_RESOURCE_FLAG_DENY_SHARED_RESOURCE return 0, 255, 255, 255 # Aqua else: return 183, 255, 255, 255 # Light aqua else: print("Unknown graphics API!") exit(1) assert False return 0, 0, 0, 255 def DrawBlock(draw, y, block, pixelsPerByte): sizePixels = int(block['Size'] * pixelsPerByte) draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + sizePixels, y + MAP_SIZE], fill=TypeToColor('FREE', 0), outline=None) byte = 0 x = 0 lastHardLineX = -1 for alloc in block['Suballocations']: byteEnd = byte + alloc['Size'] xEnd = int(byteEnd * pixelsPerByte) if alloc['Type'] != 'FREE': if xEnd > x + 1: draw.rectangle([IMG_MARGIN + x, y, IMG_MARGIN + xEnd, y + MAP_SIZE], fill=TypeToColor(alloc['Type'], alloc['Usage']), outline=COLOR_OUTLINE) # Hard line was been overwritten by rectangle outline: redraw it. if lastHardLineX == x: draw.line([IMG_MARGIN + x, y, IMG_MARGIN + x, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) else: draw.line([IMG_MARGIN + x, y, IMG_MARGIN + x, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) lastHardLineX = x byte = byteEnd x = xEnd def DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte): sizePixels = int(dedicatedAlloc['Size'] * pixelsPerByte) draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + sizePixels, y + MAP_SIZE], fill=TypeToColor(dedicatedAlloc['Type'], dedicatedAlloc['Usage']), outline=COLOR_OUTLINE) if __name__ == '__main__': args = ParseArgs() jsonSrc = json.load(open(args.DumpFile, 'rb')) if 'General' in jsonSrc: currentApi = jsonSrc['General']['API'] else: print("Wrong JSON format, cannot determine graphics API!") exit(1) # Process default pools if 'DefaultPools' in jsonSrc: for memoryPool in jsonSrc['DefaultPools'].items(): poolData = GetDataForMemoryPool(memoryPool[0]) # Get dedicated allocations for dedicatedAlloc in memoryPool[1]['DedicatedAllocations']: allocData = {'Type': dedicatedAlloc['Type'], 'Size': int(dedicatedAlloc['Size']), 'Usage': int(dedicatedAlloc['Usage'])} poolData['DedicatedAllocations'].append(allocData) # Get allocations in block vectors for block in memoryPool[1]['Blocks'].items(): ProcessBlock(poolData, block) # Process custom pools if 'CustomPools' in jsonSrc: for memoryPool in jsonSrc['CustomPools'].items(): poolData = GetDataForMemoryPool(memoryPool[0]) for pool in memoryPool[1]: poolName = pool['Name'] poolData['CustomPools'][poolName] = {'DedicatedAllocations':[], 'Blocks':[]} # Get dedicated allocations for dedicatedAlloc in pool['DedicatedAllocations']: allocData = {'Type': dedicatedAlloc['Type'], 'Size': int(dedicatedAlloc['Size']), 'Usage': int(dedicatedAlloc['Usage'])} poolData['CustomPools'][poolName]['DedicatedAllocations'].append(allocData) # Get allocations in block vectors for block in pool['Blocks'].items(): ProcessBlock(poolData['CustomPools'][poolName], block) if IsDataEmpty(): print("There is nothing to put on the image. Please make sure you generated the stats string with detailed map enabled.") exit(1) RemoveEmptyType() # Calculate dimmensions and create data image imgHeight, pixelsPerByte = CalcParams() img = Image.new('RGB', (IMG_WIDTH, imgHeight), 'white') draw = ImageDraw.Draw(img) try: font = ImageFont.truetype('segoeuib.ttf') except: font = ImageFont.load_default() # Draw grid lines bytesBetweenGridLines = 32 while bytesBetweenGridLines * pixelsPerByte < 64: bytesBetweenGridLines *= 2 byte = 0 y = IMG_MARGIN while True: x = int(byte * pixelsPerByte) if x > IMG_WIDTH - 2 * IMG_MARGIN: break draw.line([x + IMG_MARGIN, 0, x + IMG_MARGIN, imgHeight], fill=COLOR_GRID_LINE) if byte == 0: draw.text((x + IMG_MARGIN + TEXT_MARGIN, y), "0", fill=COLOR_TEXT_H2, font=font) else: text = BytesToStr(byte) textSize = draw.textsize(text, font=font) draw.text((x + IMG_MARGIN - textSize[0] - TEXT_MARGIN, y), text, fill=COLOR_TEXT_H2, font=font) byte += bytesBetweenGridLines y += FONT_SIZE + IMG_MARGIN # Draw main content for memType in sorted(data.keys()): memPoolData = data[memType] draw.text((IMG_MARGIN, y), "Memory pool %s" % memType, fill=COLOR_TEXT_H1, font=font) y += FONT_SIZE + IMG_MARGIN # Draw block vectors for block in memPoolData['Blocks']: draw.text((IMG_MARGIN, y), "Default pool block %s" % block['ID'], fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawBlock(draw, y, block, pixelsPerByte) y += MAP_SIZE + IMG_MARGIN index = 0 # Draw dedicated allocations for dedicatedAlloc in memPoolData['DedicatedAllocations']: draw.text((IMG_MARGIN, y), "Dedicated allocation %d" % index, fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte) y += MAP_SIZE + IMG_MARGIN index += 1 for poolName, pool in memPoolData['CustomPools'].items(): for block in pool['Blocks']: draw.text((IMG_MARGIN, y), "Custom pool %s block %s" % (poolName, block['ID']), fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawBlock(draw, y, block, pixelsPerByte) y += 2 * (FONT_SIZE + IMG_MARGIN) index = 0 for dedicatedAlloc in pool['DedicatedAllocations']: draw.text((IMG_MARGIN, y), "Custom pool %s dedicated allocation %d" % (poolName, index), fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawDedicatedAllocationBlock(draw, y, dedicatedAlloc, pixelsPerByte) y += MAP_SIZE + IMG_MARGIN index += 1 del draw img.save(args.output) """ Main data structure - variable `data` - is a dictionary. Key is string - memory type name. Value is dictionary of: - Fixed key 'DedicatedAllocations'. Value is list of objects, each containing dictionary with: - Fixed key 'Type'. Value is string. - Fixed key 'Size'. Value is int. - Fixed key 'Usage'. Value is int. - Fixed key 'Blocks'. Value is list of objects, each containing dictionary with: - Fixed key 'ID'. Value is int. - Fixed key 'Size'. Value is int. - Fixed key 'Suballocations'. Value is list of objects as above. - Fixed key 'CustomPools'. Value is dictionary. - Key is string with pool ID/name. Value is a dictionary with: - Fixed key 'DedicatedAllocations'. Value is list of objects as above. - Fixed key 'Blocks'. Value is a list of objects representing memory blocks as above. """