# # 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 = 'VMA Dump Visualization 2.0.1' IMG_SIZE_X = 1200 IMG_MARGIN = 8 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) argParser = argparse.ArgumentParser(description='Visualization of Vulkan Memory Allocator JSON dump.') argParser.add_argument('DumpFile', type=argparse.FileType(mode='r', encoding='UTF-8'), help='Path to source JSON file with memory dump created by Vulkan 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)') args = argParser.parse_args() data = {} def ProcessBlock(dstBlockList, iBlockId, objBlock, sAlgorithm): iBlockSize = int(objBlock['TotalBytes']) arrSuballocs = objBlock['Suballocations'] dstBlockObj = {'ID': iBlockId, 'Size':iBlockSize, 'Suballocations':[]} dstBlockObj['Algorithm'] = sAlgorithm for objSuballoc in arrSuballocs: dstBlockObj['Suballocations'].append((objSuballoc['Type'], int(objSuballoc['Size']), int(objSuballoc['Usage']) if ('Usage' in objSuballoc) else 0)) dstBlockList.append(dstBlockObj) def GetDataForMemoryType(iMemTypeIndex): global data if iMemTypeIndex in data: return data[iMemTypeIndex] else: newMemTypeData = {'DedicatedAllocations':[], 'DefaultPoolBlocks':[], 'CustomPools':{}} data[iMemTypeIndex] = newMemTypeData return newMemTypeData def IsDataEmpty(): global data for dictMemType in data.values(): if 'DedicatedAllocations' in dictMemType and len(dictMemType['DedicatedAllocations']) > 0: return False if 'DefaultPoolBlocks' in dictMemType and len(dictMemType['DefaultPoolBlocks']) > 0: return False if 'CustomPools' in dictMemType: for lBlockList in dictMemType['CustomPools'].values()['Blocks']: if len(lBlockList) > 0: return False return True # Returns tuple: # [0] image height : integer # [1] pixels per byte : float def CalcParams(): global data iImgSizeY = IMG_MARGIN iImgSizeY += FONT_SIZE + IMG_MARGIN # Grid lines legend - sizes iMaxBlockSize = 0 for dictMemType in data.values(): iImgSizeY += IMG_MARGIN + FONT_SIZE lDedicatedAllocations = dictMemType['DedicatedAllocations'] iImgSizeY += len(lDedicatedAllocations) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) for tDedicatedAlloc in lDedicatedAllocations: iMaxBlockSize = max(iMaxBlockSize, tDedicatedAlloc[1]) lDefaultPoolBlocks = dictMemType['DefaultPoolBlocks'] iImgSizeY += len(lDefaultPoolBlocks) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) for objBlock in lDefaultPoolBlocks: iMaxBlockSize = max(iMaxBlockSize, objBlock['Size']) dCustomPools = dictMemType['CustomPools'] for poolData in dCustomPools.values(): iImgSizeY += len(poolData['Blocks']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) for objBlock in poolData['Blocks']: iMaxBlockSize = max(iMaxBlockSize, objBlock['Size']) iImgSizeY += len(poolData['DedicatedAllocations']) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE) for tDedicatedAlloc in poolData['DedicatedAllocations']: iMaxBlockSize = max(iMaxBlockSize, tDedicatedAlloc[1]) fPixelsPerByte = (IMG_SIZE_X - IMG_MARGIN * 2) / float(iMaxBlockSize) return iImgSizeY, fPixelsPerByte def TypeToColor(sType, iUsage): if sType == 'FREE': return 220, 220, 220, 255 elif sType == 'BUFFER': if (iUsage & 0x1C0) != 0: # INDIRECT_BUFFER | VERTEX_BUFFER | INDEX_BUFFER return 255, 148, 148, 255 # Red elif (iUsage & 0x28) != 0: # STORAGE_BUFFER | STORAGE_TEXEL_BUFFER return 255, 187, 121, 255 # Orange elif (iUsage & 0x14) != 0: # UNIFORM_BUFFER | UNIFORM_TEXEL_BUFFER return 255, 255, 0, 255 # Yellow else: return 255, 255, 165, 255 # Light yellow elif sType == 'IMAGE_OPTIMAL': if (iUsage & 0x20) != 0: # DEPTH_STENCIL_ATTACHMENT return 246, 128, 255, 255 # Pink elif (iUsage & 0xD0) != 0: # INPUT_ATTACHMENT | TRANSIENT_ATTACHMENT | COLOR_ATTACHMENT return 179, 179, 255, 255 # Blue elif (iUsage & 0x4) != 0: # SAMPLED return 0, 255, 255, 255 # Aqua else: return 183, 255, 255, 255 # Light aqua elif sType == 'IMAGE_LINEAR': return 0, 255, 0, 255 # Green elif sType == 'IMAGE_UNKNOWN': return 0, 255, 164, 255 # Green/aqua elif sType == 'UNKNOWN': return 175, 175, 175, 255 # Gray assert False return 0, 0, 0, 255 def DrawDedicatedAllocationBlock(draw, y, tDedicatedAlloc): global fPixelsPerByte iSizeBytes = tDedicatedAlloc[1] iSizePixels = int(iSizeBytes * fPixelsPerByte) draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + iSizePixels, y + MAP_SIZE], fill=TypeToColor(tDedicatedAlloc[0], tDedicatedAlloc[2]), outline=COLOR_OUTLINE) def DrawBlock(draw, y, objBlock): global fPixelsPerByte iSizeBytes = objBlock['Size'] iSizePixels = int(iSizeBytes * fPixelsPerByte) draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + iSizePixels, y + MAP_SIZE], fill=TypeToColor('FREE', 0), outline=None) iByte = 0 iX = 0 iLastHardLineX = -1 for tSuballoc in objBlock['Suballocations']: sType = tSuballoc[0] iByteEnd = iByte + tSuballoc[1] iXEnd = int(iByteEnd * fPixelsPerByte) if sType != 'FREE': if iXEnd > iX + 1: iUsage = tSuballoc[2] draw.rectangle([IMG_MARGIN + iX, y, IMG_MARGIN + iXEnd, y + MAP_SIZE], fill=TypeToColor(sType, iUsage), outline=COLOR_OUTLINE) # Hard line was been overwritten by rectangle outline: redraw it. if iLastHardLineX == iX: draw.line([IMG_MARGIN + iX, y, IMG_MARGIN + iX, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) else: draw.line([IMG_MARGIN + iX, y, IMG_MARGIN + iX, y + MAP_SIZE], fill=COLOR_OUTLINE_HARD) iLastHardLineX = iX iByte = iByteEnd iX = iXEnd def BytesToStr(iBytes): if iBytes < 1024: return "%d B" % iBytes iBytes /= 1024 if iBytes < 1024: return "%d KiB" % iBytes iBytes /= 1024 if iBytes < 1024: return "%d MiB" % iBytes iBytes /= 1024 return "%d GiB" % iBytes jsonSrc = json.load(args.DumpFile) if 'DedicatedAllocations' in jsonSrc: for tType in jsonSrc['DedicatedAllocations'].items(): sType = tType[0] assert sType[:5] == 'Type ' iType = int(sType[5:]) typeData = GetDataForMemoryType(iType) for objAlloc in tType[1]: typeData['DedicatedAllocations'].append((objAlloc['Type'], int(objAlloc['Size']), int(objAlloc['Usage']) if ('Usage' in objAlloc) else 0)) if 'DefaultPools' in jsonSrc: for tType in jsonSrc['DefaultPools'].items(): sType = tType[0] assert sType[:5] == 'Type ' iType = int(sType[5:]) typeData = GetDataForMemoryType(iType) for sBlockId, objBlock in tType[1]['Blocks'].items(): ProcessBlock(typeData['DefaultPoolBlocks'], int(sBlockId), objBlock, '') if 'Pools' in jsonSrc: objPools = jsonSrc['Pools'] for sPoolId, objPool in objPools.items(): iType = int(objPool['MemoryTypeIndex']) typeData = GetDataForMemoryType(iType) objBlocks = objPool['Blocks'] sAlgorithm = objPool.get('Algorithm', '') sName = objPool.get('Name', None) if sName: sFullName = sPoolId + ' "' + sName + '"' else: sFullName = sPoolId typeData['CustomPools'][sFullName] = { 'Blocks':[], 'DedicatedAllocations':[] } for sBlockId, objBlock in objBlocks.items(): ProcessBlock(typeData['CustomPools'][sFullName]['Blocks'], int(sBlockId), objBlock, sAlgorithm) if 'DedicatedAllocations' in objPool: for objAlloc in objPool['DedicatedAllocations']: typeData['CustomPools'][sFullName]['DedicatedAllocations'].append((objAlloc['Type'], int(objAlloc['Size']), int(objAlloc['Usage']) if ('Usage' in objAlloc) else 0)) 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) iImgSizeY, fPixelsPerByte = CalcParams() img = Image.new('RGB', (IMG_SIZE_X, iImgSizeY), 'white') draw = ImageDraw.Draw(img) try: font = ImageFont.truetype('segoeuib.ttf') except: font = ImageFont.load_default() y = IMG_MARGIN # Draw grid lines iBytesBetweenGridLines = 32 while iBytesBetweenGridLines * fPixelsPerByte < 64: iBytesBetweenGridLines *= 2 iByte = 0 TEXT_MARGIN = 4 while True: iX = int(iByte * fPixelsPerByte) if iX > IMG_SIZE_X - 2 * IMG_MARGIN: break draw.line([iX + IMG_MARGIN, 0, iX + IMG_MARGIN, iImgSizeY], fill=COLOR_GRID_LINE) if iByte == 0: draw.text((iX + IMG_MARGIN + TEXT_MARGIN, y), "0", fill=COLOR_TEXT_H2, font=font) else: text = BytesToStr(iByte) textSize = draw.textsize(text, font=font) draw.text((iX + IMG_MARGIN - textSize[0] - TEXT_MARGIN, y), text, fill=COLOR_TEXT_H2, font=font) iByte += iBytesBetweenGridLines y += FONT_SIZE + IMG_MARGIN # Draw main content for iMemTypeIndex in sorted(data.keys()): dictMemType = data[iMemTypeIndex] draw.text((IMG_MARGIN, y), "Memory type %d" % iMemTypeIndex, fill=COLOR_TEXT_H1, font=font) y += FONT_SIZE + IMG_MARGIN index = 0 for tDedicatedAlloc in dictMemType['DedicatedAllocations']: draw.text((IMG_MARGIN, y), "Dedicated allocation %d" % index, fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawDedicatedAllocationBlock(draw, y, tDedicatedAlloc) y += MAP_SIZE + IMG_MARGIN index += 1 for objBlock in dictMemType['DefaultPoolBlocks']: draw.text((IMG_MARGIN, y), "Default pool block %d" % objBlock['ID'], fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawBlock(draw, y, objBlock) y += MAP_SIZE + IMG_MARGIN index = 0 for sPoolName, pool in dictMemType['CustomPools'].items(): for objBlock in pool['Blocks']: if 'Algorithm' in objBlock and objBlock['Algorithm']: sAlgorithm = ' (Algorithm: %s)' % (objBlock['Algorithm']) else: sAlgorithm = '' draw.text((IMG_MARGIN, y), "Custom pool %s%s block %d" % (sPoolName, sAlgorithm, objBlock['ID']), fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawBlock(draw, y, objBlock) y += 2 * (FONT_SIZE + IMG_MARGIN) index += 1 alloc_index = 0 for objAlloc in pool['DedicatedAllocations']: draw.text((IMG_MARGIN, y), "Custom pool %s%s dedicated allocation %d" % (sPoolName, sAlgorithm, alloc_index), fill=COLOR_TEXT_H2, font=font) y += FONT_SIZE + IMG_MARGIN DrawDedicatedAllocationBlock(draw, y, objAlloc) y += MAP_SIZE + IMG_MARGIN alloc_index += 1 del draw img.save(args.output) """ Main data structure - variable `data` - is a dictionary. Key is integer - memory type index. Value is dictionary of: - Fixed key 'DedicatedAllocations'. Value is list of tuples, each containing: - [0]: Type : string - [1]: Size : integer - [2]: Usage : integer (0 if unknown) - Fixed key 'DefaultPoolBlocks'. 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 tuples as above. - Fixed key 'CustomPools'. Value is dictionary. - Key is string with pool ID/name. Value is a dictionary with: - Fixed key 'Blocks'. Value is a list of objects representing memory blocks, each containing dictionary with: - Fixed key 'ID'. Value is int. - Fixed key 'Size'. Value is int. - Fixed key 'Algorithm'. Optional. Value is string. - Fixed key 'Suballocations'. Value is list of tuples as above. - Fixed key 'DedicatedAllocations'. Value is list of tuples as above. """