File: GpuMemDumpVis.py

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (334 lines) | stat: -rw-r--r-- 14,729 bytes parent folder | download | duplicates (10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#
# Copyright (c) 2018-2023 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.2'
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()):
        pool = data[poolType]
        if len(pool['DedicatedAllocations']) > 0:
           continue
        if len(pool['Blocks']) > 0:
            continue
        empty = True
        for customPool in pool['CustomPools'].values():
            if len(customPool['Blocks']) > 0:
                empty = False
                break
            if len(customPool['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 += FONT_SIZE + IMG_MARGIN # Memory pool title
        height += len(poolData['Blocks']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2)
        height += len(poolData['DedicatedAllocations']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2)
        # 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['Blocks']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2)
            height += len(customPoolData['DedicatedAllocations']) * (FONT_SIZE + MAP_SIZE + IMG_MARGIN * 2)
            # 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, (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 += MAP_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.
"""