#!/usr/bin/python
# Copyright 2018 The ANGLE Project Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# gen_vk_internal_shaders.py:
#  Code generation for internal Vulkan shaders. Should be run when an internal
#  shader program is changed, added or removed.

from datetime import date
import json
import os
import re
import subprocess
import sys

out_file_cpp = 'vk_internal_shaders_autogen.cpp'
out_file_h = 'vk_internal_shaders_autogen.h'
out_file_gni = 'vk_internal_shaders_autogen.gni'

# Templates for the generated files:
template_shader_library_cpp = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {input_file_name}
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// {out_file_name}:
//   Pre-generated shader library for the ANGLE Vulkan back-end.

#include "libANGLE/renderer/vulkan/vk_internal_shaders_autogen.h"

namespace rx
{{
namespace vk
{{
namespace
{{
{internal_shader_includes}

// This is SPIR-V binary blob and the size.
struct ShaderBlob
{{
    const uint32_t *code;
    size_t codeSize;
}};

{shader_tables_cpp}

angle::Result GetShader(Context *context,
                        RefCounted<ShaderAndSerial> *shaders,
                        const ShaderBlob *shaderBlobs,
                        size_t shadersCount,
                        uint32_t shaderFlags,
                        RefCounted<ShaderAndSerial> **shaderOut)
{{
    ASSERT(shaderFlags < shadersCount);
    RefCounted<ShaderAndSerial> &shader = shaders[shaderFlags];
    *shaderOut                          = &shader;

    if (shader.get().valid())
    {{
        return angle::Result::Continue();
    }}

    // Create shader lazily. Access will need to be locked for multi-threading.
    const ShaderBlob &shaderCode = shaderBlobs[shaderFlags];
    ASSERT(shaderCode.code != nullptr);

    return InitShaderAndSerial(context, &shader.get(), shaderCode.code, shaderCode.codeSize);
}}
}}  // anonymous namespace


ShaderLibrary::ShaderLibrary()
{{
}}

ShaderLibrary::~ShaderLibrary()
{{
}}

void ShaderLibrary::destroy(VkDevice device)
{{
    {shader_destroy_calls}
}}

{shader_get_functions_cpp}
}}  // namespace vk
}}  // namespace rx
"""

template_shader_library_h = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {input_file_name}
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// {out_file_name}:
//   Pre-generated shader library for the ANGLE Vulkan back-end.

#ifndef LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_
#define LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_

#include "libANGLE/renderer/vulkan/vk_utils.h"

namespace rx
{{
namespace vk
{{
namespace InternalShader
{{
{shader_variation_definitions}
}}  // namespace InternalShader

class ShaderLibrary final : angle::NonCopyable
{{
  public:
    ShaderLibrary();
    ~ShaderLibrary();

    void destroy(VkDevice device);

    {shader_get_functions_h}

  private:
    {shader_tables_h}
}};
}}  // namespace vk
}}  // namespace rx

#endif  // LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_
"""

template_shader_includes_gni = """# GENERATED FILE - DO NOT EDIT.
# Generated by {script_name} using data from {input_file_name}
#
# Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# {out_file_name}:
#   List of generated shaders for inclusion in ANGLE's build process.

angle_vulkan_internal_shaders = [
{shaders_list}
]
"""

# Gets the constant variable name for a generated shader.
def get_var_name(output, prefix='k'):
    return prefix + output.replace(".", "_")

# Gets the namespace name given to constants generated from shader_file
def get_namespace_name(shader_file):
    return get_var_name(os.path.basename(shader_file), '')

# Gets the namespace name given to constants generated from shader_file
def get_variation_table_name(shader_file, prefix='k'):
    return get_var_name(os.path.basename(shader_file), prefix) + '_shaders'

# Gets the internal ID string for a particular shader.
def get_shader_id(shader):
    file = os.path.splitext(os.path.basename(shader))[0]
    return file.replace(".", "_")

# Returns the name of the generated SPIR-V file for a shader.
def get_output_path(name):
    return os.path.join('shaders', 'gen', name + ".inc")

# Finds a path to GN's out directory
def find_build_path(path):
    out = os.path.join(path, "out")
    if (os.path.isdir(out)):
        for o in os.listdir(out):
            subdir = os.path.join(out, o)
            if os.path.isdir(subdir):
                argsgn = os.path.join(subdir, "args.gn")
                if os.path.isfile(argsgn):
                    return subdir
    else:
        parent = os.path.join(path, "..")
        if (os.path.isdir(parent)):
            return find_build_path(parent)
        else:
            raise Exception("Could not find GN out directory")

# Generates the code for a shader blob array entry.
def gen_shader_blob_entry(shader):
    var_name = get_var_name(os.path.basename(shader))[0:-4]
    return "{%s, %s}" % (var_name, "sizeof(%s)" % var_name)

def slash(s):
    return s.replace('\\', '/')

def gen_shader_include(shader):
    return '#include "libANGLE/renderer/vulkan/%s"' % slash(shader)

def get_shader_variations(shader):
    variation_file = shader + '.json'
    if not os.path.exists(variation_file):
        # If there is no variation file, assume none.
        return ({}, [])

    with open(variation_file) as fin:
        variations = json.loads(fin.read())
        flags = {}
        enums = []

        for key, value in variations.iteritems():
            if key == "Description":
                continue
            elif key == "Flags":
                flags = value
            elif len(value) > 0:
                enums.append((key, value))

        # sort enums so the ones with the most waste ends up last, reducing the table size
        enums.sort(key=lambda enum: (1 << (len(enum[1]) - 1).bit_length()) / float(len(enum[1])))

        return (flags, enums)

def get_variation_bits(flags, enums):
    flags_bits = len(flags)
    enum_bits = [(len(enum[1]) - 1).bit_length() for enum in enums]
    return (flags_bits, enum_bits)

def next_enum_variation(enums, enum_indices):
    """Loop through indices from [0, 0, ...] to [L0-1, L1-1, ...]
    where Li is len(enums[i]).  The list can be thought of as a number with many
    digits, where each digit is in [0, Li), and this function effectively implements
    the increment operation, with the least-significant digit being the first item."""
    for i in range(len(enums)):
        current = enum_indices[i]
        # if current digit has room, increment it.
        if current + 1 < len(enums[i][1]):
            enum_indices[i] = current + 1
            return True;
        # otherwise reset it to 0 and carry to the next digit.
        enum_indices[i] = 0

    # if this is reached, the number has overflowed and the loop is finished.
    return False

compact_newlines_regex = re.compile(r"\n\s*\n", re.MULTILINE)
def cleanup_preprocessed_shader(shader_text):
    return compact_newlines_regex.sub('\n\n', shader_text.strip())

# STEP 0: Handle inputs/outputs for run_code_generation.py's auto_script
shaders_dir = os.path.join('shaders', 'src')
if not os.path.isdir(shaders_dir):
    raise Exception("Could not find shaders directory")

valid_extensions = ['.vert', '.frag', '.comp']
input_shaders = sorted([os.path.join(shaders_dir, shader)
    for shader in os.listdir(shaders_dir)
    if any([os.path.splitext(shader)[1] == ext for ext in valid_extensions])])
if len(sys.argv) == 2 and sys.argv[1] == 'inputs':
    print(",".join(input_shaders))
    sys.exit(0)

# STEP 1: Call glslang to generate the internal shaders into small .inc files.

# a) Get the path to the glslang binary from the script directory.
build_path = find_build_path(".")
print("Using glslang_validator from '" + build_path + "'")
result = subprocess.call(['ninja', '-C', build_path, 'glslang_validator'])
if result != 0:
    raise Exception("Error building glslang_validator")

glslang_binary = 'glslang_validator'
if os.name == 'nt':
    glslang_binary += '.exe'
glslang_path = os.path.join(build_path, glslang_binary)
if not os.path.isfile(glslang_path):
    raise Exception("Could not find " + glslang_binary)

# b) Iterate over the shaders and call glslang with the right arguments.
output_shaders = []

def compile_variation(shader_file, shader_basename, flags, enums,
        flags_active, enum_indices, flags_bits, enum_bits, do_compile):

    glslang_args = [glslang_path]

    # generate -D defines and the output file name
    #
    # The variations are given a bit pattern to be able to OR different flags into a variation. The
    # least significant bits are the flags, where there is one bit per flag.  After that, each enum
    # takes up as few bits as needed to count that many enum values.
    variation_bits = 0
    variation_string = ''
    for f in range(len(flags)):
        if flags_active & (1 << f):
            flag_name = flags[f]
            glslang_args.append('-D' + flag_name + '=1')

            variation_bits |= 1 << f
            variation_string += '|' + flag_name

    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum_name = enums[e][1][enum_indices[e]]
        glslang_args.append('-D' + enum_name + '=1')

        variation_bits |= enum_indices[e] << current_bit_start
        current_bit_start += enum_bits[e]
        variation_string += '|' + enum_name

    output_name = '%s.%08X' % (shader_basename, variation_bits)
    output_path = get_output_path(output_name)
    output_shaders.append(output_path)

    if do_compile:
        glslang_preprocessor_output_args = glslang_args + ['-E']
        glslang_preprocessor_output_args.append(shader_file)           # Input GLSL shader

        glslang_args += ['-V']                                         # Output mode is Vulkan
        glslang_args += ['--variable-name', get_var_name(output_name)] # C-style variable name
        glslang_args += ['-o', output_path]                            # Output file
        glslang_args.append(shader_file)                               # Input GLSL shader

        print output_path + ': ' + shader_basename + variation_string
        result = subprocess.call(glslang_args)
        if result != 0:
            raise Exception("Error compiling " + shader_file)

        with open(output_path, 'a') as incfile:
            shader_text = subprocess.check_output(glslang_preprocessor_output_args)

            incfile.write('\n\n#if 0  // Generated from:\n')
            incfile.write(cleanup_preprocessed_shader(shader_text))
            incfile.write('\n#endif  // Preprocessed code\n')

class ShaderAndVariations:
    def __init__(self, shader_file):
        self.shader_file = shader_file
        (self.flags, self.enums) = get_shader_variations(shader_file)
        get_variation_bits(self.flags, self.enums)
        (self.flags_bits, self.enum_bits) = get_variation_bits(self.flags, self.enums)

input_shaders_and_variations = [ShaderAndVariations(shader_file) for shader_file in input_shaders]

print_outputs = len(sys.argv) == 2 and sys.argv[1] == 'outputs'

for shader_and_variation in input_shaders_and_variations:
    shader_file = shader_and_variation.shader_file
    flags = shader_and_variation.flags
    enums = shader_and_variation.enums
    flags_bits = shader_and_variation.flags_bits
    enum_bits = shader_and_variation.enum_bits

    # an array where each element i is in [0, len(enums[i])),
    # telling which enum is currently selected
    enum_indices = [0] * len(enums)

    output_name = os.path.basename(shader_file)

    while True:
        # a number where each bit says whether a flag is active or not,
        # with values in [0, 2^len(flags))
        for flags_active in range(1 << len(flags)):
            compile_variation(shader_file, output_name, flags, enums,
                    flags_active, enum_indices, flags_bits, enum_bits, not print_outputs)

        if not next_enum_variation(enums, enum_indices):
            break

output_shaders = sorted(output_shaders)
outputs = output_shaders + [out_file_cpp, out_file_h]

if print_outputs:
    print("\n".join(outputs))
    sys.exit(0)

# STEP 2: Consolidate the .inc files into an auto-generated cpp/h library.
def get_variation_definition(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    flags = shader_and_variation.flags
    enums = shader_and_variation.enums
    flags_bits = shader_and_variation.flags_bits
    enum_bits = shader_and_variation.enum_bits

    namespace_name = get_namespace_name(shader_file)

    definition = 'namespace %s\n{\n' % namespace_name
    if len(flags) > 0:
        definition += 'enum flags\n{\n'
        definition += ''.join(['k%s = 0x%08X,\n' % (flags[f], 1 << f) for f in range(len(flags))])
        definition += 'kFlagsMask = 0x%08X,\n' % ((1 << flags_bits) - 1)
        definition += '};\n'

    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum = enums[e]
        enum_name = enum[0]
        definition += 'enum %s\n{\n' % enum_name
        definition += ''.join(['k%s = 0x%08X,\n' %
            (enum[1][v], v << current_bit_start) for v in range(len(enum[1]))])
        definition += 'k%sMask = 0x%08X,\n' % (enum_name, ((1 << enum_bits[e]) - 1) << current_bit_start)
        definition += '};\n'
        current_bit_start += enum_bits[e]

    definition += '}  // namespace %s\n' % namespace_name
    return definition

def get_shader_table_h(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    flags = shader_and_variation.flags
    enums = shader_and_variation.enums

    table_name = get_variation_table_name(shader_file, 'm')

    table = 'RefCounted<ShaderAndSerial> %s[' % table_name

    namespace_name = "InternalShader::" + get_namespace_name(shader_file)

    first_or = True
    if len(flags) > 0:
        table += '%s::kFlagsMask' % namespace_name
        first_or = False

    for e in range(len(enums)):
        enum = enums[e]
        enum_name = enums[e][0]
        if not first_or:
            table += ' | '
        table += '%s::k%sMask' % (namespace_name, enum_name)
        first_or = False

    if first_or:
        table += '1'

    table += '];'
    return table

def get_shader_table_cpp(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    enums = shader_and_variation.enums
    flags_bits = shader_and_variation.flags_bits
    enum_bits = shader_and_variation.enum_bits

    # Cache max and mask value of each enum to quickly know when a possible variation is invalid
    enum_maxes = []
    enum_masks = []
    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum_values = enums[e][1]
        enum_maxes.append((len(enum_values) - 1) << current_bit_start)
        enum_masks.append(((1 << enum_bits[e]) - 1) << current_bit_start)
        current_bit_start += enum_bits[e]

    table_name = get_variation_table_name(shader_file)
    var_name = get_var_name(os.path.basename(shader_file))

    table = 'constexpr ShaderBlob %s[] = {\n' % table_name

    # The last possible variation is every flag enabled and every enum at max
    last_variation = ((1 << flags_bits) - 1) | reduce(lambda x, y: x|y, enum_maxes, 0)

    for variation in range(last_variation + 1):
        # if any variation is invalid, output an empty entry
        if any([(variation & enum_masks[e]) > enum_maxes[e] for e in range(len(enums))]):
            table += '{nullptr, 0}, // 0x%08X\n' % variation
        else:
            entry = '%s_%08X' % (var_name, variation)
            table += '{%s, sizeof(%s)},\n' % (entry, entry)

    table += '};'
    return table

def get_get_function_h(shader_and_variation):
    shader_file = shader_and_variation.shader_file

    function_name = get_var_name(os.path.basename(shader_file), 'get')

    definition = 'angle::Result %s' % function_name
    definition += '(Context *context, uint32_t shaderFlags, RefCounted<ShaderAndSerial> **shaderOut);'

    return definition

def get_get_function_cpp(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    enums = shader_and_variation.enums

    function_name = get_var_name(os.path.basename(shader_file), 'get')
    namespace_name = "InternalShader::" + get_namespace_name(shader_file)
    member_table_name = get_variation_table_name(shader_file, 'm')
    constant_table_name = get_variation_table_name(shader_file)

    definition = 'angle::Result ShaderLibrary::%s' % function_name
    definition += '(Context *context, uint32_t shaderFlags, RefCounted<ShaderAndSerial> **shaderOut)\n{\n'
    definition += 'return GetShader(context, %s, %s, ArraySize(%s), shaderFlags, shaderOut);\n}\n' % (
            member_table_name, constant_table_name, constant_table_name)

    return definition

def get_destroy_call(shader_and_variation):
    shader_file = shader_and_variation.shader_file

    table_name = get_variation_table_name(shader_file, 'm')

    destroy = 'for (RefCounted<ShaderAndSerial> &shader : %s)\n' % table_name
    destroy += '{\nshader.get().destroy(device);\n}'
    return destroy

with open(out_file_cpp, 'w') as outfile:
    includes = "\n".join([gen_shader_include(shader) for shader in output_shaders])
    shader_tables_cpp = '\n'.join([get_shader_table_cpp(s)
        for s in input_shaders_and_variations])
    shader_destroy_calls = '\n'.join([get_destroy_call(s)
        for s in input_shaders_and_variations])
    shader_get_functions_cpp = '\n'.join([get_get_function_cpp(s)
        for s in input_shaders_and_variations])

    outcode = template_shader_library_cpp.format(
        script_name = __file__,
        copyright_year = date.today().year,
        out_file_name = out_file_cpp,
        input_file_name = 'shaders/src/*',
        internal_shader_includes = includes,
        shader_tables_cpp = shader_tables_cpp,
        shader_destroy_calls = shader_destroy_calls,
        shader_get_functions_cpp = shader_get_functions_cpp)
    outfile.write(outcode)
    outfile.close()

with open(out_file_h, 'w') as outfile:
    shader_variation_definitions = '\n'.join([get_variation_definition(s)
        for s in input_shaders_and_variations])
    shader_get_functions_h = '\n'.join([get_get_function_h(s)
        for s in input_shaders_and_variations])
    shader_tables_h = '\n'.join([get_shader_table_h(s)
        for s in input_shaders_and_variations])
    outcode = template_shader_library_h.format(
        script_name = __file__,
        copyright_year = date.today().year,
        out_file_name = out_file_h,
        input_file_name = 'shaders/src/*',
        shader_variation_definitions = shader_variation_definitions,
        shader_get_functions_h = shader_get_functions_h,
        shader_tables_h = shader_tables_h)
    outfile.write(outcode)
    outfile.close()

# STEP 3: Create a gni file with the generated files.
with open(out_file_gni, 'w') as outfile:
    outcode = template_shader_includes_gni.format(
        script_name = __file__,
        copyright_year = date.today().year,
        out_file_name = out_file_gni,
        input_file_name = 'shaders/src/*',
        shaders_list = ',\n'.join(['  "' + slash(shader) + '"' for shader in output_shaders]))
    outfile.write(outcode)
    outfile.close()
