• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright 2015-2021 Arm Limited
4# SPDX-License-Identifier: Apache-2.0
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import sys
19import os
20import os.path
21import subprocess
22import tempfile
23import re
24import itertools
25import hashlib
26import shutil
27import argparse
28import codecs
29import json
30import multiprocessing
31import errno
32from functools import partial
33
34class Paths():
35    def __init__(self, spirv_cross, glslang, spirv_as, spirv_val, spirv_opt):
36        self.spirv_cross = spirv_cross
37        self.glslang = glslang
38        self.spirv_as = spirv_as
39        self.spirv_val = spirv_val
40        self.spirv_opt = spirv_opt
41
42def remove_file(path):
43    #print('Removing file:', path)
44    os.remove(path)
45
46def create_temporary(suff = ''):
47    f, path = tempfile.mkstemp(suffix = suff)
48    os.close(f)
49    #print('Creating temporary:', path)
50    return path
51
52def parse_stats(stats):
53    m = re.search('([0-9]+) work registers', stats)
54    registers = int(m.group(1)) if m else 0
55
56    m = re.search('([0-9]+) uniform registers', stats)
57    uniform_regs = int(m.group(1)) if m else 0
58
59    m_list = re.findall('(-?[0-9]+)\s+(-?[0-9]+)\s+(-?[0-9]+)', stats)
60    alu_short = float(m_list[1][0]) if m_list else 0
61    ls_short = float(m_list[1][1]) if m_list else 0
62    tex_short = float(m_list[1][2]) if m_list else 0
63    alu_long = float(m_list[2][0]) if m_list else 0
64    ls_long = float(m_list[2][1]) if m_list else 0
65    tex_long = float(m_list[2][2]) if m_list else 0
66
67    return (registers, uniform_regs, alu_short, ls_short, tex_short, alu_long, ls_long, tex_long)
68
69def get_shader_type(shader):
70    _, ext = os.path.splitext(shader)
71    if ext == '.vert':
72        return '--vertex'
73    elif ext == '.frag':
74        return '--fragment'
75    elif ext == '.comp':
76        return '--compute'
77    elif ext == '.tesc':
78        return '--tessellation_control'
79    elif ext == '.tese':
80        return '--tessellation_evaluation'
81    elif ext == '.geom':
82        return '--geometry'
83    else:
84        return ''
85
86def get_shader_stats(shader):
87    path = create_temporary()
88
89    p = subprocess.Popen(['malisc', get_shader_type(shader), '--core', 'Mali-T760', '-V', shader], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
90    stdout, stderr = p.communicate()
91    remove_file(path)
92
93    if p.returncode != 0:
94        print(stderr.decode('utf-8'))
95        raise OSError('malisc failed')
96    p.wait()
97
98    returned = stdout.decode('utf-8')
99    return parse_stats(returned)
100
101def print_msl_compiler_version():
102    try:
103        subprocess.check_call(['xcrun', '--sdk', 'iphoneos', 'metal', '--version'])
104        print('... are the Metal compiler characteristics.\n')   # display after so xcrun FNF is silent
105    except OSError as e:
106        if (e.errno != errno.ENOENT):    # Ignore xcrun not found error
107            raise
108        print('Metal SDK is not present.\n')
109    except subprocess.CalledProcessError:
110        pass
111
112def msl_compiler_supports_version(version):
113    try:
114        subprocess.check_call(['xcrun', '--sdk', 'macosx', 'metal', '-x', 'metal', '-std=macos-metal' + version, '-'],
115                stdin = subprocess.DEVNULL, stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
116        print('Current SDK supports MSL {0}. Enabling validation for MSL {0} shaders.'.format(version))
117        return True
118    except OSError as e:
119        print('Failed to check if MSL {} is not supported. It probably is not.'.format(version))
120        return False
121    except subprocess.CalledProcessError:
122        print('Current SDK does NOT support MSL {0}. Disabling validation for MSL {0} shaders.'.format(version))
123        return False
124
125def path_to_msl_standard(shader):
126    if '.ios.' in shader:
127        if '.msl2.' in shader:
128            return '-std=ios-metal2.0'
129        elif '.msl21.' in shader:
130            return '-std=ios-metal2.1'
131        elif '.msl22.' in shader:
132            return '-std=ios-metal2.2'
133        elif '.msl23.' in shader:
134            return '-std=ios-metal2.3'
135        elif '.msl11.' in shader:
136            return '-std=ios-metal1.1'
137        elif '.msl10.' in shader:
138            return '-std=ios-metal1.0'
139        else:
140            return '-std=ios-metal1.2'
141    else:
142        if '.msl2.' in shader:
143            return '-std=macos-metal2.0'
144        elif '.msl21.' in shader:
145            return '-std=macos-metal2.1'
146        elif '.msl22.' in shader:
147            return '-std=macos-metal2.2'
148        elif '.msl23.' in shader:
149            return '-std=macos-metal2.3'
150        elif '.msl11.' in shader:
151            return '-std=macos-metal1.1'
152        else:
153            return '-std=macos-metal1.2'
154
155def path_to_msl_standard_cli(shader):
156    if '.msl2.' in shader:
157        return '20000'
158    elif '.msl21.' in shader:
159        return '20100'
160    elif '.msl22.' in shader:
161        return '20200'
162    elif '.msl23.' in shader:
163        return '20300'
164    elif '.msl11.' in shader:
165        return '10100'
166    else:
167        return '10200'
168
169def validate_shader_msl(shader, opt):
170    msl_path = reference_path(shader[0], shader[1], opt)
171    try:
172        if '.ios.' in msl_path:
173            msl_os = 'iphoneos'
174        else:
175            msl_os = 'macosx'
176        subprocess.check_call(['xcrun', '--sdk', msl_os, 'metal', '-x', 'metal', path_to_msl_standard(msl_path), '-Werror', '-Wno-unused-variable', msl_path])
177        print('Compiled Metal shader: ' + msl_path)   # display after so xcrun FNF is silent
178    except OSError as oe:
179        if (oe.errno != errno.ENOENT):   # Ignore xcrun not found error
180            raise
181    except subprocess.CalledProcessError:
182        print('Error compiling Metal shader: ' + msl_path)
183        raise RuntimeError('Failed to compile Metal shader')
184
185def cross_compile_msl(shader, spirv, opt, iterations, paths):
186    spirv_path = create_temporary()
187    msl_path = create_temporary(os.path.basename(shader))
188
189    spirv_env = 'vulkan1.1spv1.4' if ('.spv14.' in shader) else 'vulkan1.1'
190
191    spirv_cmd = [paths.spirv_as, '--target-env', spirv_env, '-o', spirv_path, shader]
192    if '.preserve.' in shader:
193        spirv_cmd.append('--preserve-numeric-ids')
194
195    if spirv:
196        subprocess.check_call(spirv_cmd)
197    else:
198        subprocess.check_call([paths.glslang, '--amb' ,'--target-env', 'vulkan1.1', '-V', '-o', spirv_path, shader])
199
200    if opt and (not shader_is_invalid_spirv(shader)):
201        if '.graphics-robust-access.' in shader:
202            subprocess.check_call([paths.spirv_opt, '--skip-validation', '-O', '--graphics-robust-access', '-o', spirv_path, spirv_path])
203        else:
204            subprocess.check_call([paths.spirv_opt, '--skip-validation', '-O', '-o', spirv_path, spirv_path])
205
206    spirv_cross_path = paths.spirv_cross
207
208    msl_args = [spirv_cross_path, '--output', msl_path, spirv_path, '--msl', '--iterations', str(iterations)]
209    msl_args.append('--msl-version')
210    msl_args.append(path_to_msl_standard_cli(shader))
211    if not '.nomain.' in shader:
212        msl_args.append('--entry')
213        msl_args.append('main')
214    if '.swizzle.' in shader:
215        msl_args.append('--msl-swizzle-texture-samples')
216    if '.ios.' in shader:
217        msl_args.append('--msl-ios')
218    if '.pad-fragment.' in shader:
219        msl_args.append('--msl-pad-fragment-output')
220    if '.capture.' in shader:
221        msl_args.append('--msl-capture-output')
222    if '.domain.' in shader:
223        msl_args.append('--msl-domain-lower-left')
224    if '.argument.' in shader:
225        msl_args.append('--msl-argument-buffers')
226    if '.texture-buffer-native.' in shader:
227        msl_args.append('--msl-texture-buffer-native')
228    if '.framebuffer-fetch.' in shader:
229        msl_args.append('--msl-framebuffer-fetch')
230    if '.invariant-float-math.' in shader:
231        msl_args.append('--msl-invariant-float-math')
232    if '.emulate-cube-array.' in shader:
233        msl_args.append('--msl-emulate-cube-array')
234    if '.discrete.' in shader:
235        # Arbitrary for testing purposes.
236        msl_args.append('--msl-discrete-descriptor-set')
237        msl_args.append('2')
238        msl_args.append('--msl-discrete-descriptor-set')
239        msl_args.append('3')
240    if '.force-active.' in shader:
241        msl_args.append('--msl-force-active-argument-buffer-resources')
242    if '.line.' in shader:
243        msl_args.append('--emit-line-directives')
244    if '.multiview.' in shader:
245        msl_args.append('--msl-multiview')
246    if '.no-layered.' in shader:
247        msl_args.append('--msl-multiview-no-layered-rendering')
248    if '.viewfromdev.' in shader:
249        msl_args.append('--msl-view-index-from-device-index')
250    if '.dispatchbase.' in shader:
251        msl_args.append('--msl-dispatch-base')
252    if '.dynamic-buffer.' in shader:
253        # Arbitrary for testing purposes.
254        msl_args.append('--msl-dynamic-buffer')
255        msl_args.append('0')
256        msl_args.append('0')
257        msl_args.append('--msl-dynamic-buffer')
258        msl_args.append('1')
259        msl_args.append('2')
260    if '.inline-block.' in shader:
261        # Arbitrary for testing purposes.
262        msl_args.append('--msl-inline-uniform-block')
263        msl_args.append('0')
264        msl_args.append('0')
265    if '.device-argument-buffer.' in shader:
266        msl_args.append('--msl-device-argument-buffer')
267        msl_args.append('0')
268        msl_args.append('--msl-device-argument-buffer')
269        msl_args.append('1')
270    if '.force-native-array.' in shader:
271        msl_args.append('--msl-force-native-arrays')
272    if '.zero-initialize.' in shader:
273        msl_args.append('--force-zero-initialized-variables')
274    if '.frag-output.' in shader:
275        # Arbitrary for testing purposes.
276        msl_args.append('--msl-disable-frag-depth-builtin')
277        msl_args.append('--msl-disable-frag-stencil-ref-builtin')
278        msl_args.append('--msl-enable-frag-output-mask')
279        msl_args.append('0x000000ca')
280    if '.no-user-varying.' in shader:
281        msl_args.append('--msl-no-clip-distance-user-varying')
282    if '.shader-inputs.' in shader:
283        # Arbitrary for testing purposes.
284        msl_args.append('--msl-shader-input')
285        msl_args.append('0')
286        msl_args.append('u8')
287        msl_args.append('2')
288        msl_args.append('--msl-shader-input')
289        msl_args.append('1')
290        msl_args.append('u16')
291        msl_args.append('3')
292        msl_args.append('--msl-shader-input')
293        msl_args.append('6')
294        msl_args.append('other')
295        msl_args.append('4')
296    if '.multi-patch.' in shader:
297        msl_args.append('--msl-multi-patch-workgroup')
298        # Arbitrary for testing purposes.
299        msl_args.append('--msl-shader-input')
300        msl_args.append('0')
301        msl_args.append('any32')
302        msl_args.append('3')
303        msl_args.append('--msl-shader-input')
304        msl_args.append('1')
305        msl_args.append('any16')
306        msl_args.append('2')
307    if '.for-tess.' in shader:
308        msl_args.append('--msl-vertex-for-tessellation')
309    if '.fixed-sample-mask.' in shader:
310        msl_args.append('--msl-additional-fixed-sample-mask')
311        msl_args.append('0x00000022')
312    if '.arrayed-subpass.' in shader:
313        msl_args.append('--msl-arrayed-subpass-input')
314    if '.1d-as-2d.' in shader:
315        msl_args.append('--msl-texture-1d-as-2d')
316    if '.simd.' in shader:
317        msl_args.append('--msl-ios-use-simdgroup-functions')
318    if '.emulate-subgroup.' in shader:
319        msl_args.append('--msl-emulate-subgroups')
320    if '.fixed-subgroup.' in shader:
321        # Arbitrary for testing purposes.
322        msl_args.append('--msl-fixed-subgroup-size')
323        msl_args.append('32')
324    if '.force-sample.' in shader:
325        msl_args.append('--msl-force-sample-rate-shading')
326    if '.decoration-binding.' in shader:
327        msl_args.append('--msl-decoration-binding')
328    if '.mask-location-0.' in shader:
329        msl_args.append('--mask-stage-output-location')
330        msl_args.append('0')
331        msl_args.append('0')
332    if '.mask-location-1.' in shader:
333        msl_args.append('--mask-stage-output-location')
334        msl_args.append('1')
335        msl_args.append('0')
336    if '.mask-position.' in shader:
337        msl_args.append('--mask-stage-output-builtin')
338        msl_args.append('Position')
339    if '.mask-point-size.' in shader:
340        msl_args.append('--mask-stage-output-builtin')
341        msl_args.append('PointSize')
342    if '.mask-clip-distance.' in shader:
343        msl_args.append('--mask-stage-output-builtin')
344        msl_args.append('ClipDistance')
345
346    subprocess.check_call(msl_args)
347
348    if not shader_is_invalid_spirv(msl_path):
349        subprocess.check_call([paths.spirv_val, '--scalar-block-layout', '--target-env', spirv_env, spirv_path])
350
351    return (spirv_path, msl_path)
352
353def shader_model_hlsl(shader):
354    if '.vert' in shader:
355        if '.sm30.' in shader:
356            return '-Tvs_3_0'
357        else:
358            return '-Tvs_5_1'
359    elif '.frag' in shader:
360        if '.sm30.' in shader:
361            return '-Tps_3_0'
362        else:
363            return '-Tps_5_1'
364    elif '.comp' in shader:
365        return '-Tcs_5_1'
366    else:
367        return None
368
369def shader_to_win_path(shader):
370    # It's (very) convenient to be able to run HLSL testing in wine on Unix-likes, so support that.
371    try:
372        with subprocess.Popen(['winepath', '-w', shader], stdout = subprocess.PIPE, stderr = subprocess.PIPE) as f:
373            stdout_data, stderr_data = f.communicate()
374            return stdout_data.decode('utf-8')
375    except OSError as oe:
376        if (oe.errno != errno.ENOENT): # Ignore not found errors
377            return shader
378    except subprocess.CalledProcessError:
379        raise
380
381    return shader
382
383ignore_fxc = False
384def validate_shader_hlsl(shader, force_no_external_validation, paths):
385    test_glslang = True
386    if '.nonuniformresource.' in shader:
387        test_glslang = False
388    if '.fxconly.' in shader:
389        test_glslang = False
390
391    hlsl_args = [paths.glslang, '--amb', '-e', 'main', '-D', '--target-env', 'vulkan1.1', '-V', shader]
392    if '.sm30.' in shader:
393        hlsl_args.append('--hlsl-dx9-compatible')
394
395    if test_glslang:
396        subprocess.check_call(hlsl_args)
397
398    is_no_fxc = '.nofxc.' in shader
399    global ignore_fxc
400    if (not ignore_fxc) and (not force_no_external_validation) and (not is_no_fxc):
401        try:
402            win_path = shader_to_win_path(shader)
403            args = ['fxc', '-nologo', shader_model_hlsl(shader), win_path]
404            if '.nonuniformresource.' in shader:
405                args.append('/enable_unbounded_descriptor_tables')
406            subprocess.check_call(args)
407        except OSError as oe:
408            if (oe.errno != errno.ENOENT): # Ignore not found errors
409                print('Failed to run FXC.')
410                ignore_fxc = True
411                raise
412            else:
413                print('Could not find FXC.')
414                ignore_fxc = True
415        except subprocess.CalledProcessError:
416            print('Failed compiling HLSL shader:', shader, 'with FXC.')
417            raise RuntimeError('Failed compiling HLSL shader')
418
419def shader_to_sm(shader):
420    if '.sm62.' in shader:
421        return '62'
422    elif '.sm60.' in shader:
423        return '60'
424    elif '.sm51.' in shader:
425        return '51'
426    elif '.sm30.' in shader:
427        return '30'
428    else:
429        return '50'
430
431def cross_compile_hlsl(shader, spirv, opt, force_no_external_validation, iterations, paths):
432    spirv_path = create_temporary()
433    hlsl_path = create_temporary(os.path.basename(shader))
434
435    spirv_env = 'vulkan1.1spv1.4' if '.spv14.' in shader else 'vulkan1.1'
436    spirv_cmd = [paths.spirv_as, '--target-env', spirv_env, '-o', spirv_path, shader]
437    if '.preserve.' in shader:
438        spirv_cmd.append('--preserve-numeric-ids')
439
440    if spirv:
441        subprocess.check_call(spirv_cmd)
442    else:
443        subprocess.check_call([paths.glslang, '--amb', '--target-env', 'vulkan1.1', '-V', '-o', spirv_path, shader])
444
445    if opt and (not shader_is_invalid_spirv(hlsl_path)):
446        subprocess.check_call([paths.spirv_opt, '--skip-validation', '-O', '-o', spirv_path, spirv_path])
447
448    spirv_cross_path = paths.spirv_cross
449
450    sm = shader_to_sm(shader)
451
452    hlsl_args = [spirv_cross_path, '--entry', 'main', '--output', hlsl_path, spirv_path, '--hlsl-enable-compat', '--hlsl', '--shader-model', sm, '--iterations', str(iterations)]
453    if '.line.' in shader:
454        hlsl_args.append('--emit-line-directives')
455    if '.force-uav.' in shader:
456        hlsl_args.append('--hlsl-force-storage-buffer-as-uav')
457    if '.zero-initialize.' in shader:
458        hlsl_args.append('--force-zero-initialized-variables')
459    if '.nonwritable-uav-texture.' in shader:
460        hlsl_args.append('--hlsl-nonwritable-uav-texture-as-srv')
461    if '.native-16bit.' in shader:
462        hlsl_args.append('--hlsl-enable-16bit-types')
463    if '.flatten-matrix-vertex-input.' in shader:
464        hlsl_args.append('--hlsl-flatten-matrix-vertex-input-semantics')
465
466    subprocess.check_call(hlsl_args)
467
468    if not shader_is_invalid_spirv(hlsl_path):
469        subprocess.check_call([paths.spirv_val, '--scalar-block-layout', '--target-env', spirv_env, spirv_path])
470
471    validate_shader_hlsl(hlsl_path, force_no_external_validation, paths)
472
473    return (spirv_path, hlsl_path)
474
475def cross_compile_reflect(shader, spirv, opt, iterations, paths):
476    spirv_path = create_temporary()
477    reflect_path = create_temporary(os.path.basename(shader))
478
479    spirv_cmd = [paths.spirv_as, '--target-env', 'vulkan1.1', '-o', spirv_path, shader]
480    if '.preserve.' in shader:
481        spirv_cmd.append('--preserve-numeric-ids')
482
483    if spirv:
484        subprocess.check_call(spirv_cmd)
485    else:
486        subprocess.check_call([paths.glslang, '--amb', '--target-env', 'vulkan1.1', '-V', '-o', spirv_path, shader])
487
488    if opt and (not shader_is_invalid_spirv(reflect_path)):
489        subprocess.check_call([paths.spirv_opt, '--skip-validation', '-O', '-o', spirv_path, spirv_path])
490
491    spirv_cross_path = paths.spirv_cross
492
493    sm = shader_to_sm(shader)
494    subprocess.check_call([spirv_cross_path, '--entry', 'main', '--output', reflect_path, spirv_path, '--reflect', '--iterations', str(iterations)])
495    return (spirv_path, reflect_path)
496
497def validate_shader(shader, vulkan, paths):
498    if vulkan:
499        spirv_14 = '.spv14.' in shader
500        glslang_env = 'spirv1.4' if spirv_14 else 'vulkan1.1'
501        subprocess.check_call([paths.glslang, '--amb', '--target-env', glslang_env, '-V', shader])
502    else:
503        subprocess.check_call([paths.glslang, shader])
504
505def cross_compile(shader, vulkan, spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo, sso, flatten_dim, opt, push_ubo, iterations, paths):
506    spirv_path = create_temporary()
507    glsl_path = create_temporary(os.path.basename(shader))
508
509    spirv_14 = '.spv14.' in shader
510    spirv_env = 'vulkan1.1spv1.4' if spirv_14 else 'vulkan1.1'
511
512    if vulkan or spirv:
513        vulkan_glsl_path = create_temporary('vk' + os.path.basename(shader))
514
515    spirv_cmd = [paths.spirv_as, '--target-env', spirv_env, '-o', spirv_path, shader]
516    if '.preserve.' in shader:
517        spirv_cmd.append('--preserve-numeric-ids')
518
519    if spirv:
520        subprocess.check_call(spirv_cmd)
521    else:
522        glslang_env = 'spirv1.4' if spirv_14 else 'vulkan1.1'
523        subprocess.check_call([paths.glslang, '--amb', '--target-env', glslang_env, '-V', '-o', spirv_path, shader])
524
525    if opt and (not invalid_spirv):
526        subprocess.check_call([paths.spirv_opt, '--skip-validation', '-O', '-o', spirv_path, spirv_path])
527
528    if not invalid_spirv:
529        subprocess.check_call([paths.spirv_val, '--scalar-block-layout', '--target-env', spirv_env, spirv_path])
530
531    extra_args = ['--iterations', str(iterations)]
532    if eliminate:
533        extra_args += ['--remove-unused-variables']
534    if is_legacy:
535        extra_args += ['--version', '100', '--es']
536    if flatten_ubo:
537        extra_args += ['--flatten-ubo']
538    if sso:
539        extra_args += ['--separate-shader-objects']
540    if flatten_dim:
541        extra_args += ['--flatten-multidimensional-arrays']
542    if push_ubo:
543        extra_args += ['--glsl-emit-push-constant-as-ubo']
544    if '.line.' in shader:
545        extra_args += ['--emit-line-directives']
546    if '.no-samplerless.' in shader:
547        extra_args += ['--vulkan-glsl-disable-ext-samplerless-texture-functions']
548    if '.no-qualifier-deduction.' in shader:
549        extra_args += ['--disable-storage-image-qualifier-deduction']
550    if '.framebuffer-fetch.' in shader:
551        extra_args += ['--glsl-remap-ext-framebuffer-fetch', '0', '0']
552        extra_args += ['--glsl-remap-ext-framebuffer-fetch', '1', '1']
553        extra_args += ['--glsl-remap-ext-framebuffer-fetch', '2', '2']
554        extra_args += ['--glsl-remap-ext-framebuffer-fetch', '3', '3']
555    if '.framebuffer-fetch-noncoherent.' in shader:
556        extra_args += ['--glsl-ext-framebuffer-fetch-noncoherent']
557    if '.zero-initialize.' in shader:
558        extra_args += ['--force-zero-initialized-variables']
559    if '.force-flattened-io.' in shader:
560        extra_args += ['--glsl-force-flattened-io-blocks']
561
562    spirv_cross_path = paths.spirv_cross
563
564    # A shader might not be possible to make valid GLSL from, skip validation for this case.
565    if (not ('nocompat' in glsl_path)) or (not vulkan):
566        subprocess.check_call([spirv_cross_path, '--entry', 'main', '--output', glsl_path, spirv_path] + extra_args)
567        if not 'nocompat' in glsl_path:
568            validate_shader(glsl_path, False, paths)
569    else:
570        remove_file(glsl_path)
571        glsl_path = None
572
573    if (vulkan or spirv) and (not is_legacy):
574        subprocess.check_call([spirv_cross_path, '--entry', 'main', '-V', '--output', vulkan_glsl_path, spirv_path] + extra_args)
575        validate_shader(vulkan_glsl_path, True, paths)
576        # SPIR-V shaders might just want to validate Vulkan GLSL output, we don't always care about the output.
577        if not vulkan:
578            remove_file(vulkan_glsl_path)
579
580    return (spirv_path, glsl_path, vulkan_glsl_path if vulkan else None)
581
582def make_unix_newline(buf):
583    decoded = codecs.decode(buf, 'utf-8')
584    decoded = decoded.replace('\r', '')
585    return codecs.encode(decoded, 'utf-8')
586
587def md5_for_file(path):
588    md5 = hashlib.md5()
589    with open(path, 'rb') as f:
590        for chunk in iter(lambda: make_unix_newline(f.read(8192)), b''):
591            md5.update(chunk)
592    return md5.digest()
593
594def make_reference_dir(path):
595    base = os.path.dirname(path)
596    if not os.path.exists(base):
597        os.makedirs(base)
598
599def reference_path(directory, relpath, opt):
600    split_paths = os.path.split(directory)
601    reference_dir = os.path.join(split_paths[0], 'reference/' + ('opt/' if opt else ''))
602    reference_dir = os.path.join(reference_dir, split_paths[1])
603    return os.path.join(reference_dir, relpath)
604
605def regression_check_reflect(shader, json_file, args):
606    reference = reference_path(shader[0], shader[1], args.opt) + '.json'
607    joined_path = os.path.join(shader[0], shader[1])
608    print('Reference shader reflection path:', reference)
609    if os.path.exists(reference):
610        actual = md5_for_file(json_file)
611        expected = md5_for_file(reference)
612        if actual != expected:
613            if args.update:
614                print('Generated reflection json has changed for {}!'.format(reference))
615                # If we expect changes, update the reference file.
616                if os.path.exists(reference):
617                    remove_file(reference)
618                make_reference_dir(reference)
619                shutil.move(json_file, reference)
620            else:
621                print('Generated reflection json in {} does not match reference {}!'.format(json_file, reference))
622                with open(json_file, 'r') as f:
623                    print('')
624                    print('Generated:')
625                    print('======================')
626                    print(f.read())
627                    print('======================')
628                    print('')
629
630                # Otherwise, fail the test. Keep the shader file around so we can inspect.
631                if not args.keep:
632                    remove_file(json_file)
633
634                raise RuntimeError('Does not match reference')
635        else:
636            remove_file(json_file)
637    else:
638        print('Found new shader {}. Placing generated source code in {}'.format(joined_path, reference))
639        make_reference_dir(reference)
640        shutil.move(json_file, reference)
641
642def regression_check(shader, glsl, args):
643    reference = reference_path(shader[0], shader[1], args.opt)
644    joined_path = os.path.join(shader[0], shader[1])
645    print('Reference shader path:', reference)
646
647    if os.path.exists(reference):
648        if md5_for_file(glsl) != md5_for_file(reference):
649            if args.update:
650                print('Generated source code has changed for {}!'.format(reference))
651                # If we expect changes, update the reference file.
652                if os.path.exists(reference):
653                    remove_file(reference)
654                make_reference_dir(reference)
655                shutil.move(glsl, reference)
656            else:
657                print('Generated source code in {} does not match reference {}!'.format(glsl, reference))
658                with open(glsl, 'r') as f:
659                    print('')
660                    print('Generated:')
661                    print('======================')
662                    print(f.read())
663                    print('======================')
664                    print('')
665
666                # Otherwise, fail the test. Keep the shader file around so we can inspect.
667                if not args.keep:
668                    remove_file(glsl)
669                raise RuntimeError('Does not match reference')
670        else:
671            remove_file(glsl)
672    else:
673        print('Found new shader {}. Placing generated source code in {}'.format(joined_path, reference))
674        make_reference_dir(reference)
675        shutil.move(glsl, reference)
676
677def shader_is_vulkan(shader):
678    return '.vk.' in shader
679
680def shader_is_desktop(shader):
681    return '.desktop.' in shader
682
683def shader_is_eliminate_dead_variables(shader):
684    return '.noeliminate.' not in shader
685
686def shader_is_spirv(shader):
687    return '.asm.' in shader
688
689def shader_is_invalid_spirv(shader):
690    return '.invalid.' in shader
691
692def shader_is_legacy(shader):
693    return '.legacy.' in shader
694
695def shader_is_flatten_ubo(shader):
696    return '.flatten.' in shader
697
698def shader_is_sso(shader):
699    return '.sso.' in shader
700
701def shader_is_flatten_dimensions(shader):
702    return '.flatten_dim.' in shader
703
704def shader_is_noopt(shader):
705    return '.noopt.' in shader
706
707def shader_is_push_ubo(shader):
708    return '.push-ubo.' in shader
709
710def test_shader(stats, shader, args, paths):
711    joined_path = os.path.join(shader[0], shader[1])
712    vulkan = shader_is_vulkan(shader[1])
713    desktop = shader_is_desktop(shader[1])
714    eliminate = shader_is_eliminate_dead_variables(shader[1])
715    is_spirv = shader_is_spirv(shader[1])
716    invalid_spirv = shader_is_invalid_spirv(shader[1])
717    is_legacy = shader_is_legacy(shader[1])
718    flatten_ubo = shader_is_flatten_ubo(shader[1])
719    sso = shader_is_sso(shader[1])
720    flatten_dim = shader_is_flatten_dimensions(shader[1])
721    noopt = shader_is_noopt(shader[1])
722    push_ubo = shader_is_push_ubo(shader[1])
723
724    print('Testing shader:', joined_path)
725    spirv, glsl, vulkan_glsl = cross_compile(joined_path, vulkan, is_spirv, invalid_spirv, eliminate, is_legacy, flatten_ubo, sso, flatten_dim, args.opt and (not noopt), push_ubo, args.iterations, paths)
726
727    # Only test GLSL stats if we have a shader following GL semantics.
728    if stats and (not vulkan) and (not is_spirv) and (not desktop):
729        cross_stats = get_shader_stats(glsl)
730
731    if glsl:
732        regression_check(shader, glsl, args)
733    if vulkan_glsl:
734        regression_check((shader[0], shader[1] + '.vk'), vulkan_glsl, args)
735
736    remove_file(spirv)
737
738    if stats and (not vulkan) and (not is_spirv) and (not desktop):
739        pristine_stats = get_shader_stats(joined_path)
740
741        a = []
742        a.append(shader[1])
743        for i in pristine_stats:
744            a.append(str(i))
745        for i in cross_stats:
746            a.append(str(i))
747        print(','.join(a), file = stats)
748
749def test_shader_msl(stats, shader, args, paths):
750    joined_path = os.path.join(shader[0], shader[1])
751    print('\nTesting MSL shader:', joined_path)
752    is_spirv = shader_is_spirv(shader[1])
753    noopt = shader_is_noopt(shader[1])
754    spirv, msl = cross_compile_msl(joined_path, is_spirv, args.opt and (not noopt), args.iterations, paths)
755    regression_check(shader, msl, args)
756
757    # Uncomment the following line to print the temp SPIR-V file path.
758    # This temp SPIR-V file is not deleted until after the Metal validation step below.
759    # If Metal validation fails, the temp SPIR-V file can be copied out and
760    # used as input to an invocation of spirv-cross to debug from Xcode directly.
761    # To do so, build spriv-cross using `make DEBUG=1`, then run the spriv-cross
762    # executable from Xcode using args: `--msl --entry main --output msl_path spirv_path`.
763#    print('SPRIV shader: ' + spirv)
764
765    shader_is_msl22 = 'msl22' in joined_path
766    shader_is_msl23 = 'msl23' in joined_path
767    skip_validation = (shader_is_msl22 and (not args.msl22)) or (shader_is_msl23 and (not args.msl23))
768    if '.invalid.' in joined_path:
769        skip_validation = True
770
771    if (not args.force_no_external_validation) and (not skip_validation):
772        validate_shader_msl(shader, args.opt)
773
774    remove_file(spirv)
775
776def test_shader_hlsl(stats, shader, args, paths):
777    joined_path = os.path.join(shader[0], shader[1])
778    print('Testing HLSL shader:', joined_path)
779    is_spirv = shader_is_spirv(shader[1])
780    noopt = shader_is_noopt(shader[1])
781    spirv, hlsl = cross_compile_hlsl(joined_path, is_spirv, args.opt and (not noopt), args.force_no_external_validation, args.iterations, paths)
782    regression_check(shader, hlsl, args)
783    remove_file(spirv)
784
785def test_shader_reflect(stats, shader, args, paths):
786    joined_path = os.path.join(shader[0], shader[1])
787    print('Testing shader reflection:', joined_path)
788    is_spirv = shader_is_spirv(shader[1])
789    noopt = shader_is_noopt(shader[1])
790    spirv, reflect = cross_compile_reflect(joined_path, is_spirv, args.opt and (not noopt), args.iterations, paths)
791    regression_check_reflect(shader, reflect, args)
792    remove_file(spirv)
793
794def test_shader_file(relpath, stats, args, backend):
795    paths = Paths(args.spirv_cross, args.glslang, args.spirv_as, args.spirv_val, args.spirv_opt)
796    try:
797        if backend == 'msl':
798            test_shader_msl(stats, (args.folder, relpath), args, paths)
799        elif backend == 'hlsl':
800            test_shader_hlsl(stats, (args.folder, relpath), args, paths)
801        elif backend == 'reflect':
802            test_shader_reflect(stats, (args.folder, relpath), args, paths)
803        else:
804            test_shader(stats, (args.folder, relpath), args, paths)
805        return None
806    except Exception as e:
807        return e
808
809def test_shaders_helper(stats, backend, args):
810    all_files = []
811    for root, dirs, files in os.walk(os.path.join(args.folder)):
812        files = [ f for f in files if not f.startswith(".") ]   #ignore system files (esp OSX)
813        for i in files:
814            path = os.path.join(root, i)
815            relpath = os.path.relpath(path, args.folder)
816            all_files.append(relpath)
817
818    # The child processes in parallel execution mode don't have the proper state for the global args variable, so
819    # at this point we need to switch to explicit arguments
820    if args.parallel:
821        with multiprocessing.Pool(multiprocessing.cpu_count()) as pool:
822            results = []
823            for f in all_files:
824                results.append(pool.apply_async(test_shader_file,
825                    args = (f, stats, args, backend)))
826
827            pool.close()
828            pool.join()
829            results_completed = [res.get() for res in results]
830
831            for error in results_completed:
832                if error is not None:
833                    print('Error:', error)
834                    sys.exit(1)
835
836    else:
837        for i in all_files:
838            e = test_shader_file(i, stats, args, backend)
839            if e is not None:
840                print('Error:', e)
841                sys.exit(1)
842
843def test_shaders(backend, args):
844    if args.malisc:
845        with open('stats.csv', 'w') as stats:
846            print('Shader,OrigRegs,OrigUniRegs,OrigALUShort,OrigLSShort,OrigTEXShort,OrigALULong,OrigLSLong,OrigTEXLong,CrossRegs,CrossUniRegs,CrossALUShort,CrossLSShort,CrossTEXShort,CrossALULong,CrossLSLong,CrossTEXLong', file = stats)
847            test_shaders_helper(stats, backend, args)
848    else:
849        test_shaders_helper(None, backend, args)
850
851def main():
852    parser = argparse.ArgumentParser(description = 'Script for regression testing.')
853    parser.add_argument('folder',
854            help = 'Folder containing shader files to test.')
855    parser.add_argument('--update',
856            action = 'store_true',
857            help = 'Updates reference files if there is a mismatch. Use when legitimate changes in output is found.')
858    parser.add_argument('--keep',
859            action = 'store_true',
860            help = 'Leave failed GLSL shaders on disk if they fail regression. Useful for debugging.')
861    parser.add_argument('--malisc',
862            action = 'store_true',
863            help = 'Use malisc offline compiler to determine static cycle counts before and after spirv-cross.')
864    parser.add_argument('--msl',
865            action = 'store_true',
866            help = 'Test Metal backend.')
867    parser.add_argument('--metal',
868            action = 'store_true',
869            help = 'Deprecated Metal option. Use --msl instead.')
870    parser.add_argument('--hlsl',
871            action = 'store_true',
872            help = 'Test HLSL backend.')
873    parser.add_argument('--force-no-external-validation',
874            action = 'store_true',
875            help = 'Disable all external validation.')
876    parser.add_argument('--opt',
877            action = 'store_true',
878            help = 'Run SPIRV-Tools optimization passes as well.')
879    parser.add_argument('--reflect',
880            action = 'store_true',
881            help = 'Test reflection backend.')
882    parser.add_argument('--parallel',
883            action = 'store_true',
884            help = 'Execute tests in parallel.  Useful for doing regression quickly, but bad for debugging and stat output.')
885    parser.add_argument('--spirv-cross',
886            default = './spirv-cross',
887            help = 'Explicit path to spirv-cross')
888    parser.add_argument('--glslang',
889            default = 'glslangValidator',
890            help = 'Explicit path to glslangValidator')
891    parser.add_argument('--spirv-as',
892            default = 'spirv-as',
893            help = 'Explicit path to spirv-as')
894    parser.add_argument('--spirv-val',
895            default = 'spirv-val',
896            help = 'Explicit path to spirv-val')
897    parser.add_argument('--spirv-opt',
898            default = 'spirv-opt',
899            help = 'Explicit path to spirv-opt')
900    parser.add_argument('--iterations',
901            default = 1,
902            type = int,
903            help = 'Number of iterations to run SPIRV-Cross (benchmarking)')
904
905    args = parser.parse_args()
906    if not args.folder:
907        sys.stderr.write('Need shader folder.\n')
908        sys.exit(1)
909
910    if (args.parallel and (args.malisc or args.force_no_external_validation or args.update)):
911        sys.stderr.write('Parallel execution is disabled when using the flags --update, --malisc or --force-no-external-validation\n')
912        args.parallel = False
913
914    args.msl22 = False
915    args.msl23 = False
916    if args.msl:
917        print_msl_compiler_version()
918        args.msl22 = msl_compiler_supports_version('2.2')
919        args.msl23 = msl_compiler_supports_version('2.3')
920
921    backend = 'glsl'
922    if (args.msl or args.metal):
923        backend = 'msl'
924    elif args.hlsl:
925        backend = 'hlsl'
926    elif args.reflect:
927        backend = 'reflect'
928
929    test_shaders(backend, args)
930    if args.malisc:
931        print('Stats in stats.csv!')
932    print('Tests completed!')
933
934if __name__ == '__main__':
935    main()
936