Implement initial version of D3d12maDumpVis visualization script

Also a minor fix: Add missing mutex lock to BlockVector::IsEmpty.
diff --git a/src/D3D12MemAlloc.cpp b/src/D3D12MemAlloc.cpp
index 5aee696..e428eaf 100644
--- a/src/D3D12MemAlloc.cpp
+++ b/src/D3D12MemAlloc.cpp
@@ -2023,7 +2023,7 @@
     UINT GetHeapType() const { return m_HeapType; }

     UINT64 GetPreferredBlockSize() const { return m_PreferredBlockSize; }

 

-    bool IsEmpty() const { return m_Blocks.empty(); }

+    bool IsEmpty();

 

     HRESULT Allocate(

         UINT64 size,

@@ -2957,6 +2957,12 @@
     return S_OK;

 }

 

+bool BlockVector::IsEmpty()

+{

+    MutexLockRead lock(m_Mutex, m_hAllocator->UseMutex());

+    return m_Blocks.empty();

+}

+

 HRESULT BlockVector::Allocate(

     UINT64 size,

     UINT64 alignment,

diff --git a/tools/D3d12maDumpVis/D3d12maDumpVis.py b/tools/D3d12maDumpVis/D3d12maDumpVis.py
new file mode 100644
index 0000000..08e79a9
--- /dev/null
+++ b/tools/D3d12maDumpVis/D3d12maDumpVis.py
@@ -0,0 +1,284 @@
+#

+# Copyright (c) 2019 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 = 'D3D12MA Dump Visualization 1.0.0'

+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 D3D12 Memory Allocator JSON dump.')

+argParser.add_argument('DumpFile', type=argparse.FileType(mode='r', encoding='utf_16_le'), help='Path to source JSON file with memory dump created by 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)')

+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.get('Flags', 0)), int(objSuballoc.get('Layout', 0))))

+    dstBlockList.append(dstBlockObj)

+

+

+def GetDataForHeapType(sHeapType):

+    global data

+    if sHeapType in data:

+        return data[sHeapType]

+    else:

+        newHeapTypeData = {'CommittedAllocations':[], 'DefaultPoolBlocks':[], 'CustomPools':{}}

+        data[sHeapType] = newHeapTypeData

+        return newHeapTypeData

+

+

+# 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['CommittedAllocations']

+        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 lBlocks in dCustomPools.values():

+            iImgSizeY += len(lBlocks) * (IMG_MARGIN * 2 + FONT_SIZE + MAP_SIZE)

+            for objBlock in lBlocks:

+                iMaxBlockSize = max(iMaxBlockSize, objBlock['Size'])

+        """

+    fPixelsPerByte = (IMG_SIZE_X - IMG_MARGIN * 2) / float(iMaxBlockSize)

+    return iImgSizeY, fPixelsPerByte

+

+

+def TypeToColor(sType, iFlags, iLayout):

+    if sType == 'FREE':

+        return 220, 220, 220, 255

+    elif sType == 'BUFFER':

+        return 255, 255, 0, 255 # Yellow

+    elif sType == 'TEXTURE2D' or sType == 'TEXTURE1D' or sType == 'TEXTURE3D':

+        if iLayout != 0: # D3D12_TEXTURE_LAYOUT_UNKNOWN

+            return 0, 255, 0, 255 # Green

+        else:

+            if (iFlags & 0x2) != 0: # D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL

+                return 246, 128, 255, 255 # Pink

+            elif (iFlags & 0x5) != 0: # D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS

+                return 179, 179, 255, 255 # Blue

+            elif (iFlags & 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:

+        return 175, 175, 175, 255 # Gray

+    assert False

+    return 0, 0, 0, 255

+

+

+def DrawCommittedAllocationBlock(draw, y, tAlloc): 

+    global fPixelsPerByte

+    iSizeBytes = tAlloc[1]

+    iSizePixels = int(iSizeBytes * fPixelsPerByte)

+    draw.rectangle([IMG_MARGIN, y, IMG_MARGIN + iSizePixels, y + MAP_SIZE], fill=TypeToColor(tAlloc[0], tAlloc[2], tAlloc[3]), 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, 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:

+                iFlags = tSuballoc[2]

+                iLayout = tSuballoc[3]

+                draw.rectangle([IMG_MARGIN + iX, y, IMG_MARGIN + iXEnd, y + MAP_SIZE], fill=TypeToColor(sType, iFlags, iLayout), 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 KB" % iBytes

+    iBytes /= 1024

+    if iBytes < 1024:

+        return "%d MB" % iBytes

+    iBytes /= 1024

+    return "%d GB" % iBytes

+

+

+jsonSrc = json.load(args.DumpFile)

+objDetailedMap = jsonSrc['DetailedMap']

+if 'CommittedAllocations' in objDetailedMap:

+    for tType in objDetailedMap['CommittedAllocations'].items():

+        sHeapType = tType[0]

+        typeData = GetDataForHeapType(sHeapType)

+        for objAlloc in tType[1]:

+            typeData['CommittedAllocations'].append((objAlloc['Type'], int(objAlloc['Size']), int(objAlloc.get('Flags', 0)), int(objAlloc.get('Layout', 0))))

+if 'DefaultPools' in objDetailedMap:

+    for tType in objDetailedMap['DefaultPools'].items():

+        sHeapType = tType[0]

+        typeData = GetDataForHeapType(sHeapType)

+        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 = GetDataForHeapType(iType)

+        objBlocks = objPool['Blocks']

+        sAlgorithm = objPool.get('Algorithm', '')

+        sName = objPool.get('Name', None)

+        if sName:

+            sFullName = sPoolId + ' "' + sName + '"'

+        else:

+            sFullName = sPoolId

+        dstBlockArray = []

+        typeData['CustomPools'][sFullName] = dstBlockArray

+        for sBlockId, objBlock in objBlocks.items():

+            ProcessBlock(dstBlockArray, int(sBlockId), objBlock, sAlgorithm)

+"""

+

+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 sHeapType in data.keys():

+    dictMemType = data[sHeapType]

+    draw.text((IMG_MARGIN, y), sHeapType, fill=COLOR_TEXT_H1, font=font)

+    y += FONT_SIZE + IMG_MARGIN

+    index = 0

+    for tCommittedAlloc in dictMemType['CommittedAllocations']:

+        draw.text((IMG_MARGIN, y), "Committed allocation %d" % index, fill=COLOR_TEXT_H2, font=font)

+        y += FONT_SIZE + IMG_MARGIN

+        DrawCommittedAllocationBlock(draw, y, tCommittedAlloc)

+        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, listPool in dictMemType['CustomPools'].items():

+        for objBlock in listPool:

+            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 += MAP_SIZE + IMG_MARGIN

+            index += 1

+    """

+del draw

+img.save(args.output)

+

+"""

+Main data structure - variable `data` - is a dictionary. Key is string - heap type ('DEFAULT', 'UPLOAD', or 'READBACK'). Value is dictionary of:

+- Fixed key 'CommittedAllocations'. Value is list of tuples, each containing:

+    - [0]: Type : string

+    - [1]: Size : integer

+    - [2]: Flags : integer (0 if unknown)

+    - [3]: Layout : 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.

+X- Fixed key 'CustomPools'. Value is dictionary.

+  - Key is string with pool ID/name. Value is 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.

+"""