[Mesa-dev] [PATCH 10/10] spirv: Generate SPIR-V builder infrastructure

Ian Romanick idr at freedesktop.org
Tue Nov 14 00:00:08 UTC 2017


On 11/13/2017 02:01 PM, Dylan Baker wrote:
> Quoting Ian Romanick (2017-11-10 14:32:50)
> [snip]
>> +
>> +def can_have_extra_operands(name, enums):
>> +    """Determine whether an enum can have extra operands.
>> +
>> +    Some enums, like SpvDecoration, can have extra operands with some values.
>> +    Other enums, like SpvMemorySemantics, cannot.
>> +    """
>> +
>> +    if name not in enums:
>> +        return False
>> +
>> +    for parameter in enums[name][1]:
>> +        if len(parameter[2]) > 0:
> 
> Not using len() here would be faster:
> if parameter[2]:
>     return True
> 
> Or, if you like a more functional approach you could do something like:
> return any(p[2] for p in enums[name][1])
> 
>> +            return True
>> +
>> +    return False
>> +
>> +def prototype_for_instruction_emit(inst, all_enums, bit_enums):
>> +    parameters = ["spirv_program *prog"]
>> +
>> +    if "operands" in inst:
>> +        parameters = parameters + [declare_parameter(operand, all_enums, bit_enums) for operand in inst["operands"]]
>> +
>> +    return textwrap.dedent("""\
>> +    static inline void
>> +    emit_Spv{name}({parameters})""".format(name=inst["opname"],
>> +                                           parameters=",\n        ".join(parameters)))
>> +
>> +
>> +def default_value(parameter, bit_enums):
>> +    """Determine whether an enum has a value that is the default when the enum is
>> +    not specified.
>> +
>> +    Some enums are almost always marked as optional on the instructions that
>> +    use them.  Some of these, like SpvMemoryAccess, have a value that is
>> +    assumed when the value is not specificed in the instruction.
>> +    """
>> +
>> +    pass
> 
> This function appears to be unused.
> 
>> +
>> +def instruction_size(instruction, enums):
>> +    """Determine the size of an instruction based on its operands
>> +
>> +    Instructions that have only operands without ? or * quantifiers are sized
>> +    by the number of operands.  In addition, instructions that have certain
>> +    BitEnum and ValueEnum parameters also have varying sizes.
>> +    """
>> +
>> +    if "operands" not in instruction:
>> +        # Instructions like OpNop that have no operands.  Handle these with a
>> +        # trivial special case.
>> +        return 1
>> +
>> +    for operand in instruction["operands"]:
>> +        if "quantifier" in operand:
>> +            return 0
>> +        elif operand["kind"] == "LiteralString":
>> +            return 0
>> +        elif operand["kind"] in enums and enums[operand["kind"]][0]:
>> +            return 0
>> +
>> +    return len(instruction["operands"]) + 1
>> +
>> +def optional_parameter_flag(operand, bit_enums):
>> +    """Return the name of the flag for an optional parameter, None otherwise
>> +
>> +    Optional parameters are noted in the JSON with a "?" quantifier.  For some
>> +    types, it is possible to infer that a value does not exist by examining
>> +    the value.  For example, if a optional LiteralString parameter is NULL, it
>> +    is not included.  The same applies for some BitEnum kinds such as
>> +    ImageOperands. For cases where the existence cannot be infered from the
>> +    value, a "has_foo" flag is added to the parameter list.
>> +
>> +    This function returns either the has_foo name as a string or None.
>> +    """
>> +
>> +    if operand["kind"] == "LiteralString":
>> +        return None
>> +
>> +    if operand["kind"] == "ImageOperands":
>> +        return None
>> +
>> +    if operand["kind"] in bit_enums:
>> +        return None
>> +
>> +    if "quantifier" not in operand or operand["quantifier"] != "?":
>> +        return None
>> +
>> +    return "has_" + operand_name(operand)
>> +
>> +def operand_name(operand):
>> +    if operand["kind"] == "IdResult":
>> +        return "result"
>> +    elif operand["kind"] == "IdResultType":
>> +        return "result_type"
>> +    elif "name" in operand:
>> +        if "quantifier" in operand and "+" in operand["name"]:
>> +            return operand["kind"]
>> +        elif operand["name"] == "'D~ref~'":
>> +            return "deref"
>> +        else:
>> +            name=operand["name"].replace("'", "").replace(" ", "_").replace(".", "").replace("~","_").lower()
>> +            return name if name not in ["default"] else "_" + name
>> +    else:
>> +        return operand["kind"]
>> +
>> +def list_is_same(a, b):
>> +    if len(a) != len(b):
>> +        return False
>> +
>> +    for x in a:
>> +        if x not in b:
>> +            return False
>> +
>> +    return True
> 
> is there a reason you canot just do `a == b`?

That's what I had first, and it did not work in some cases.  I believe
that list==list is only True if the elements have the same order.  This
function only requires that both lists have the same contents without
regard for order.  I will add a comment to that effect.

>> +
>> +def capabilities_are_same(enums):
>> +    for parameter in enums[1:]:
>> +        if not list_is_same(enums[0][1], parameter[1]):
>> +            return False
>> +
>> +    return True
>> +
>> +def render_enable_capability(capability_list, indent="   "):
>> +    if len(capability_list) == 0:
>> +        return ""
>> +
>> +    execution_modes = ["Kernel", "Shader", "Geometry", "Tessellation"]
>> +
>> +    if set(capability_list) <= set(execution_modes):
>> +        # If all of the capabilities are execution mode capabilities, then one
>> +        # of the must already be set.
>> +
>> +        if len(capability_list) == 1:
>> +            text = "assert(prog->capabilities->is_enabled(SpvCapability{}));".format(capability_list[0])
>> +        else:
>> +            template = Template("""\
>> +            assert(prog->capabilities->is_enabled(SpvCapability${capability_list[0]}) ||
>> +            % for cap in capability_list[1:-1]:
>> +                   prog->capabilities->is_enabled(SpvCapability${cap}) ||
>> +            % endfor
>> +                   prog->capabilities->is_enabled(SpvCapability${capability_list[-1]}));""")
>> +
>> +            text = template.render(capability_list=capability_list)
>> +    elif len(capability_list) == 2 and capability_list[0] in execution_modes:
>> +        # If there are two capabilities listed and one is either
>> +        # SpvCapabilityKernel or SpvCapabilityShader, then enable the other
>> +        # capability if the non-SpvCapabilityKernel-or-SpvCapabilityShader
>> +        # capablity is not enabled.
>> +
>> +        text = """\
>> +        if (!prog->capabilities->is_enabled(SpvCapability{}))
>> +           prog->capabilities->enable(SpvCapability{});""".format(capability_list[0],
>> +                                                                  capability_list[1])
>> +    elif len(capability_list) == 1:
>> +        # If there is a single capability listed that is neither
>> +        # SpvCapabilityKernel nor SpvCapabilityShader, then enable it.
>> +
>> +        text = "prog->capabilities->enable(SpvCapability{});".format(capability_list[0])
>> +    else:
>> +        # If there are multiple capabilities listed, things become
>> +        # complicated.  There is generally not enough information available to
>> +        # know which capability should be enabled.
>> +
>> +        template = Template("""\
>> +        if (!prog->capabilities->is_enabled(SpvCapability${capability_list[0]}) &&
>> +        % for cap in capability_list[1:-1]:
>> +            !prog->capabilities->is_enabled(SpvCapability${cap}) &&
>> +        % endfor
>> +            !prog->capabilities->is_enabled(SpvCapability${capability_list[-1]})) {
>> +           // FINISHME: Do something smart.
>> +        }""")
>> +
>> +        text = template.render(capability_list=capability_list)
>> +
>> +    text = textwrap.dedent(text)
>> +    return "\n".join([(indent + l).rstrip() for l in text.splitlines()])
>> +
>> +TEMPLATE  = Template(COPYRIGHT + """\
>> +#ifndef SPIRV_BUILDER_FUNCTIONS_H
>> +#define SPIRV_BUILDER_FUNCTIONS_H
>> +
>> +% for name in bit_enums:
>> +
>> +static inline void
>> +Spv${name}_validate_and_set_capabilities(MAYBE_UNUSED spirv_program *prog, MAYBE_UNUSED ${type_map[name]} bits${", MAYBE_UNUSED const uint32_t *parameters" if bit_enums[name][0] else ""})
>> +{
>> +   assert((bits & ~(${bit_enums[name][1][0][0]} |
>> +    % for parameter in bit_enums[name][1][1:]:
>> +                    ${parameter[0]}${" |" if parameter != bit_enums[name][1][-1] else ")) == 0);"}
>> +    % endfor
>> +    % for parameter in bit_enums[name][1]:
>> +        % if len(parameter[1]) == 1 and parameter[1][0] in ["Shader", "Kernel"]:
>> +   assert((bits & ${parameter[0]}) == 0 ||
>> +          prog->capabilities->is_enabled(SpvCapability${parameter[1][0]}));
>> +        % elif len(parameter[1]) > 0:
>> +
>> +   if ((bits & ${parameter[0]}))
>> +      prog->capabilities->enable(SpvCapability${parameter[1][0]});
>> +        % endif
>> +    % endfor
>> +}
>> +    % if bit_enums[name][0]:
>> +
>> +static inline unsigned
>> +${type_map[name]}_parameter_count(${type_map[name]} bits)
>> +{
>> +   unsigned count = 0;
>> +    % for parameter in bit_enums[name][1]:
>> +        % if len(parameter[2]) > 0:
>> +
>> +   if ((bits & ${parameter[0]}) != 0)
>> +      count += ${len(parameter[2])};
>> +        % endif
>> +    % endfor
>> +
>> +   return count;
>> +}
>> +    % endif
>> +% endfor
>> +% for name in value_enums:
>> +
>> +static inline void
>> +Spv${name}_validate_and_set_capabilities(MAYBE_UNUSED spirv_program *prog, ${type_map[name]} value${", MAYBE_UNUSED const uint32_t *parameters" if value_enums[name][0] else ""})
>> +{
>> +   switch (value) {
>> +    % if name == "Capability":
>> +        % for parameter in value_enums[name][1]:
>> +   case ${type_map[name]}${parameter[0]}:
>> +        % endfor
>> +      break;
>> +    % else:
>> +        % for parameter in value_enums[name][1]:
>> +   case ${type_map[name]}${parameter[0]}:
>> +            % for i in range(len(parameter[2])):
>> +                % if parameter[2][i]["kind"] in all_enums:
>> +      Spv${parameter[2][i]["kind"]}_validate_and_set_capabilities(prog, (${type_map[parameter[2][i]["kind"]]}) parameters[${i}]);
>> +                % endif
>> +            % endfor
>> +            % if not capabilities_are_same(value_enums[name][1]) or parameter == value_enums[name][1][-1]:
>> +                % if len(parameter[1]) > 0:
>> +${render_enable_capability(parameter[1], "      ")}
>> +                % endif
>> +      break;
>> +            % endif
>> +        % endfor
>> +    % endif
>> +   default:
>> +      unreachable("Invalid ${type_map[name]} value.");
>> +   }
>> +}
>> +    % if value_enums[name][0]:
>> +
>> +static inline unsigned
>> +Spv${name}_parameter_count(${type_map[name]} value)
>> +{
>> +   switch (value) {
>> +        % for parameter in value_enums[name][1]:
>> +   case ${type_map[name]}${parameter[0]}: return ${len(parameter[2])};
>> +        % endfor
>> +   default: unreachable("Invalid ${type_map[name]} value.");
>> +   }
>> +}
>> +    % endif
>> +% endfor
>> +
>> +static inline void
>> +emit_SpvHeader(spirv_program *prog, uint32_t magic, uint32_t id_bound)
>> +{
>> +   prog->write_word(SpvMagicNumber);
>> +   prog->write_word(SpvVersion);
>> +   prog->write_word(magic);
>> +   prog->write_word(id_bound);
>> +   prog->write_word(0);
>> +}
>> +% for inst in instructions:
>> +
>> +    % if "operands" in inst and "quantifier" in inst["operands"][-1] and inst["operands"][-1]["quantifier"] == "*":
>> +static inline void
>> +begin_Spv${inst["opname"]}(
>> +   ${",    ".join(["spirv_program *prog"] + [declare_parameter(operand, all_enums, bit_enums) for operand in inst["operands"][:-1]])})
>> +{
>> +        % if "capabilities" in inst:
>> +${render_enable_capability(inst["capabilities"], "   ")}
>> +
>> +        % endif
>> +
>> +       % if len(inst["operands"]) < 2:
>> +   /* Write the instruction header now, even though it will be rewritten in
>> +    * end_Spv${inst["opname"]}, to aid debugging.  Having something in the
>> +    * output stream can be helpful.
>> +    */
>> +        % endif
>> +   prog->start_instruction(Spv${inst["opname"]});
>> +        % for operand in inst["operands"][:-1]:
>> +            % if operand["kind"] == "LiteralString":
>> +   prog->write_string(${operand_name(operand)});
>> +            % else:
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +            % endif
>> +        % endfor
>> +}
>> +
>> +static inline void
>> +emit_Spv${inst["opname"]}_data(
>> +   spirv_program *prog,
>> +        % if inst["operands"][-1]["kind"] in composite_types:
>> +            % for i in range(len(composite_types[inst["operands"][-1]["kind"]]) - 1):
>> +   ${type_map[composite_types[inst["operands"][-1]["kind"]][i]]} p${i},
>> +            % endfor
>> +   ${type_map[composite_types[inst["operands"][-1]["kind"]][-1]]} p${len(composite_types[inst["operands"][-1]["kind"]]) - 1})
>> +        % else:
>> +   ${declare_parameter(inst["operands"][-1], all_enums, bit_enums)})
>> +        % endif
>> +{
>> +   //assert(prog->begin != NULL);
>> +        % if inst["operands"][-1]["kind"] in composite_types:
>> +            % for i in range(len(composite_types[inst["operands"][-1]["kind"]])):
>> +   prog->write_word((uint32_t) p${i});
>> +            % endfor
>> +        % else:
>> +   prog->write_word((uint32_t) ${operand_name(inst["operands"][-1])});
>> +        % endif
>> +}
>> +
>> +static inline void
>> +end_Spv${inst["opname"]}(spirv_program *prog)
>> +{
>> +   prog->finish_instruction(Spv${inst["opname"]});
>> +}
>> +    % elif instruction_size(inst, all_enums) == 0:
>> +${prototype_for_instruction_emit(inst, all_enums, bit_enums)}
>> +{
>> +        % if "capabilities" in inst:
>> +${render_enable_capability(inst["capabilities"], "   ")}
>> +
>> +        % endif
>> +        % if "operands" in inst:
>> +            % for operand in inst["operands"]:
>> +                % if operand["kind"] in all_enums:
>> +                    % if optional_parameter_flag(operand, bit_enums) is not None:
>> +   if (${optional_parameter_flag(operand, bit_enums)})
>> +      Spv${operand["kind"]}_validate_and_set_capabilities(prog, ${operand_name(operand)}${", (const uint32_t *){}_parameters".format(operand_name(operand)) if can_have_extra_operands(operand["kind"], all_enums) else ""});
>> +                    % else:
>> +   Spv${operand["kind"]}_validate_and_set_capabilities(prog, ${operand_name(operand)}${", (const uint32_t *){}_parameters".format(operand_name(operand)) if can_have_extra_operands(operand["kind"], all_enums) else ""});
>> +                    % endif
>> +                % endif
>> +            % endfor
>> +
>> +        % endif
>> +        % if instruction_size(inst, all_enums) == 0:
>> +   prog->start_instruction(Spv${inst["opname"]});
>> +        % else:
>> +   prog->start_instruction(Spv${inst["opname"]}, ${instruction_size(inst, all_enums)});
>> +        % endif
>> +        % if "operands" in inst:
>> +            % for operand in inst["operands"]:
>> +                % if optional_parameter_flag(operand, bit_enums) is not None:
>> +
>> +   if (${optional_parameter_flag(operand, bit_enums)})
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +                % elif operand["kind"] == "LiteralString" and "quantifier" in operand and operand["quantifier"] == "?":
>> +
>> +   if (${operand_name(operand)} != NULL)
>> +      prog->write_string(${operand_name(operand)});
>> +                % elif operand["kind"] == "LiteralString":
>> +   prog->write_string(${operand_name(operand)});
>> +                % elif operand["kind"] in bit_enums and bit_enums[operand["kind"]][0]:
>> +                    % if "quantifier" in operand and operand["quantifier"] == "?":
>> +
>> +   if (${operand_name(operand)} != ${type_map[operand["kind"]]}None) {
>> +      prog->write_word((uint32_t) ${operand_name(operand)});
>> +      prog->write(${operand_name(operand)}_parameters,
>> +                  ${type_map[operand["kind"]]}_parameter_count(${operand_name(operand)}));
>> +   }
>> +                    % else:
>> +
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +                        % if operand["kind"] == "ImageOperands":
>> +   assert(${operand_name(operand)} != ${type_map[operand["kind"]]}None);
>> +
>> +   prog->write(${operand_name(operand)}_parameters,
>> +               ${type_map[operand["kind"]]}_parameter_count(${operand_name(operand)}));
>> +                        % else:
>> +   if (${operand_name(operand)} != ${type_map[operand["kind"]]}None) {
>> +      prog->write(${operand_name(operand)}_parameters,
>> +                  ${type_map[operand["kind"]]}_parameter_count(${operand_name(operand)}));
>> +   }
>> +                        % endif
>> +                    % endif
>> +                % elif operand["kind"] in value_enums and value_enums[operand["kind"]][0]:
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +   prog->write(${operand_name(operand)}_parameters,
>> +               ${type_map[operand["kind"]]}_parameter_count(${operand_name(operand)}));
>> +                % else:
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +                % endif
>> +            % endfor
>> +        % endif
>> +
>> +   /* Fix the size based on what was actually emitted. */
>> +   prog->finish_instruction(Spv${inst["opname"]});
>> +}
>> +    % else:
>> +${prototype_for_instruction_emit(inst, all_enums, bit_enums)}
>> +{
>> +        % if "capabilities" in inst:
>> +${render_enable_capability(inst["capabilities"], "   ")}
>> +
>> +        % endif
>> +        % if inst["opname"] == "OpTypeInt":
>> +   switch (width) {
>> +   case 8:
>> +      assert(prog->capabilities->is_enabled(SpvCapabilityKernel));
>> +      prog->capabilities->enable(SpvCapabilityInt8);
>> +      break;
>> +   case 16:
>> +      prog->capabilities->enable(SpvCapabilityInt16);
>> +      break;
>> +   case 32:
>> +      break;
>> +   case 64:
>> +      prog->capabilities->enable(SpvCapabilityInt64);
>> +      break;
>> +   default:
>> +      assert(!"Invalid integer width.");
>> +   }
>> +
>> +        % elif inst["opname"] == "OpTypeFloat":
>> +   switch (width) {
>> +   case 16:
>> +      prog->capabilities->enable(SpvCapabilityFloat16);
>> +      break;
>> +   case 32:
>> +      break;
>> +   case 64:
>> +      prog->capabilities->enable(SpvCapabilityFloat64);
>> +      break;
>> +   default:
>> +      assert(!"Invalid float width.");
>> +   }
>> +
>> +        % endif
>> +        % if "operands" in inst:
>> +            % for operand in inst["operands"]:
>> +                % if operand["kind"] in all_enums:
>> +   Spv${operand["kind"]}_validate_and_set_capabilities(prog, ${operand_name(operand)}${", (const uint32_t *){}_parameters".format(operand_name(operand)) if can_have_extra_operands(operand["kind"], all_enums) else ""});
>> +                % endif
>> +            % endfor
>> +
>> +        % endif
>> +   prog->start_instruction(Spv${inst["opname"]}, ${instruction_size(inst, all_enums)});
>> +        % if "operands" in inst:
>> +            % for operand in inst["operands"]:
>> +   prog->write_word((uint32_t) ${operand_name(operand)});
>> +            % endfor
>> +        % endif
>> +   prog->finish_instruction(Spv${inst["opname"]});
>> +}
>> +    % endif
>> +% endfor
>> +
>> +#endif /* SPIRV_BUILDER_FUNCTIONS_H */
>> +""")
>> +
>> +if __name__ == "__main__":
>> +    pargs = parse_args()
>> +
>> +    spirv_info = json.JSONDecoder().decode(open(pargs.json, "r").read())
>> +
>> +    type_map = {"LiteralContextDependentNumber": "int",
>> +                "LiteralExtInstInteger": "int",
>> +                "LiteralInteger": "int",
>> +                "LiteralString": "const char *",
>> +                "LiteralSpecConstantOpInteger": "int"
>> +    }
>> +
>> +    composite_types = {}
>> +    bit_enums = {}
>> +    value_enums = {}
>> +    for operand_kind in spirv_info["operand_kinds"]:
>> +        kind = operand_kind["kind"]
>> +
>> +        if operand_kind["category"] == "BitEnum":
>> +            type_map[kind] = "Spv" + kind + "Mask"
>> +
>> +            values = []
>> +            has_parameters = False
>> +            for enum in operand_kind["enumerants"]:
>> +                # For some reason, spirv.h does not include names for 0-values
>> +                # of BitEnums that are not None.  For example, there is no
>> +                # SpvMemorySemanticsRelaxedMask even though it appears in the
>> +                # JSON.
>> +                if int(enum["value"], 16) == 0 and enum["enumerant"] != "None":
>> +                    continue
>> +
>> +                capabilities = enum["capabilities"] if "capabilities" in enum else []
>> +                parameters = enum["parameters"] if "parameters" in enum else []
>> +
>> +                if enum["enumerant"] == "None":
>> +                    enum_name = "Spv" + kind + "MaskNone"
>> +                else:
>> +                    enum_name = "Spv" + kind + enum["enumerant"] + "Mask"
>> +
>> +                values.append((enum_name, capabilities, parameters))
>> +
>> +                if len(parameters) > 0:
> 
> just `if parameters:`
> 
>> +                    has_parameters = True
>> +
>> +            bit_enums[kind] = (has_parameters, values)
>> +        elif operand_kind["category"] == "ValueEnum":
>> +            type_map[kind] = "Spv" + kind
>> +
>> +            values = []
>> +            has_parameters = False
>> +            last_value = -1
>> +            for enum in operand_kind["enumerants"]:
>> +                if enum["value"] == last_value:
>> +                    continue
>> +
>> +                last_value = enum["value"]
>> +                capabilities = enum["capabilities"] if "capabilities" in enum else []
>> +                parameters = enum["parameters"] if "parameters" in enum else []
>> +                values.append((enum["enumerant"], capabilities, parameters))
>> +
>> +                if len(parameters) > 0:
>> +                    has_parameters = True
>> +
>> +            value_enums[kind] = (has_parameters, values)
>> +        elif operand_kind["category"] == "Id":
>> +            type_map[kind] = "SpvId"
>> +        elif operand_kind["category"] == "Composite":
>> +            type_map[kind] = "struct Spv" + kind
>> +            composite_types[kind] = operand_kind["bases"]
>> +        elif operand_kind["category"] != "Literal" or kind not in type_map:
>> +            print("Don't know how to handle {} in category {}".format(kind, operand_kind["category"]))
>> +            exit(1)
>> +
>> +    all_enums = dict(value_enums);
>> +    all_enums.update(bit_enums);
>> +
>> +    with open(pargs.out, 'w') as f:
>> +        f.write(TEMPLATE.render(instructions=spirv_info["instructions"],
>> +                                composite_types=composite_types,
>> +                                type_map=type_map,
>> +                                operand_name=operand_name,
>> +                                instruction_size=instruction_size,
>> +                                optional_parameter_flag=optional_parameter_flag,
>> +                                declare_parameter=declare_parameter,
>> +                                can_have_extra_operands=can_have_extra_operands,
>> +                                prototype_for_instruction_emit=prototype_for_instruction_emit,
>> +                                bit_enums=bit_enums,
>> +                                value_enums=value_enums,
>> +                                all_enums=all_enums,
>> +                                capabilities_are_same=capabilities_are_same,
>> +                                render_enable_capability=render_enable_capability))
>> -- 
>> 2.9.5
>>
>> _______________________________________________
>> mesa-dev mailing list
>> mesa-dev at lists.freedesktop.org
>> https://lists.freedesktop.org/mailman/listinfo/mesa-dev


-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 181 bytes
Desc: OpenPGP digital signature
URL: <https://lists.freedesktop.org/archives/mesa-dev/attachments/20171113/0c829a8a/attachment-0001.sig>


More information about the mesa-dev mailing list