dawn-cmake/generator/dawn_json_generator.py
Austin Eng a9e39e11a8 Add utilities for printing Dawn enums and bitmasks
This is a header-only utility so it can be easily included and
used anyhere. Include it in DawnTest.h to automatically pick up
definitions to print test parameters.

Also, work around bot limitations for very-long test names.

Bug: none
Change-Id: I940263ab0a4cc415b06fa04749694f16ff08335c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/51841
Auto-Submit: Austin Eng <enga@chromium.org>
Reviewed-by: Stephen White <senorblanco@chromium.org>
Commit-Queue: Austin Eng <enga@chromium.org>
2021-06-01 18:49:12 +00:00

851 lines
29 KiB
Python

#!/usr/bin/env python3
# Copyright 2017 The Dawn Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json, os, sys
from collections import namedtuple
from generator_lib import Generator, run_generator, FileRender
############################################################
# OBJECT MODEL
############################################################
class Name:
def __init__(self, name, native=False):
self.native = native
self.name = name
if native:
self.chunks = [name]
else:
self.chunks = name.split(' ')
def get(self):
return self.name
def CamelChunk(self, chunk):
return chunk[0].upper() + chunk[1:]
def canonical_case(self):
return (' '.join(self.chunks)).lower()
def concatcase(self):
return ''.join(self.chunks)
def camelCase(self):
return self.chunks[0] + ''.join(
[self.CamelChunk(chunk) for chunk in self.chunks[1:]])
def CamelCase(self):
return ''.join([self.CamelChunk(chunk) for chunk in self.chunks])
def SNAKE_CASE(self):
return '_'.join([chunk.upper() for chunk in self.chunks])
def snake_case(self):
return '_'.join(self.chunks)
def js_enum_case(self):
result = self.chunks[0].lower()
for chunk in self.chunks[1:]:
if not result[-1].isdigit():
result += '-'
result += chunk.lower()
return result
def concat_names(*names):
return ' '.join([name.canonical_case() for name in names])
class Type:
def __init__(self, name, json_data, native=False):
self.json_data = json_data
self.dict_name = name
self.name = Name(name, native=native)
self.category = json_data['category']
self.javascript = self.json_data.get('javascript', True)
EnumValue = namedtuple('EnumValue', ['name', 'value', 'valid', 'jsrepr'])
class EnumType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data)
self.values = []
self.contiguousFromZero = True
lastValue = -1
for m in self.json_data['values']:
value = m['value']
if value != lastValue + 1:
self.contiguousFromZero = False
lastValue = value
self.values.append(
EnumValue(Name(m['name']), value, m.get('valid', True),
m.get('jsrepr', None)))
# Assert that all values are unique in enums
all_values = set()
for value in self.values:
if value.value in all_values:
raise Exception("Duplicate value {} in enum {}".format(
value.value, name))
all_values.add(value.value)
BitmaskValue = namedtuple('BitmaskValue', ['name', 'value'])
class BitmaskType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data)
self.values = [
BitmaskValue(Name(m['name']), m['value'])
for m in self.json_data['values']
]
self.full_mask = 0
for value in self.values:
self.full_mask = self.full_mask | value.value
class CallbackType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data)
self.arguments = []
class TypedefType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data)
self.type = None
class NativeType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data, native=True)
# Methods and structures are both "records", so record members correspond to
# method arguments or structure members.
class RecordMember:
def __init__(self,
name,
typ,
annotation,
optional=False,
is_return_value=False,
default_value=None,
skip_serialize=False):
self.name = name
self.type = typ
self.annotation = annotation
self.length = None
self.optional = optional
self.is_return_value = is_return_value
self.handle_type = None
self.default_value = default_value
self.skip_serialize = skip_serialize
def set_handle_type(self, handle_type):
assert self.type.dict_name == "ObjectHandle"
self.handle_type = handle_type
Method = namedtuple('Method', ['name', 'return_type', 'arguments'])
class ObjectType(Type):
def __init__(self, name, json_data):
Type.__init__(self, name, json_data)
self.methods = []
self.built_type = None
class Record:
def __init__(self, name):
self.name = Name(name)
self.members = []
self.may_have_dawn_object = False
def update_metadata(self):
def may_have_dawn_object(member):
if isinstance(member.type, ObjectType):
return True
elif isinstance(member.type, StructureType):
return member.type.may_have_dawn_object
else:
return False
self.may_have_dawn_object = any(
may_have_dawn_object(member) for member in self.members)
# Set may_have_dawn_object to true if the type is chained or
# extensible. Chained structs may contain a Dawn object.
if isinstance(self, StructureType):
self.may_have_dawn_object = (self.may_have_dawn_object
or self.chained or self.extensible)
class StructureType(Record, Type):
def __init__(self, name, json_data):
Record.__init__(self, name)
Type.__init__(self, name, json_data)
self.chained = json_data.get("chained", False)
self.extensible = json_data.get("extensible", False)
# Chained structs inherit from wgpu::ChainedStruct, which has
# nextInChain, so setting both extensible and chained would result in
# two nextInChain members.
assert not (self.extensible and self.chained)
class Command(Record):
def __init__(self, name, members=None):
Record.__init__(self, name)
self.members = members or []
self.derived_object = None
self.derived_method = None
def linked_record_members(json_data, types):
members = []
members_by_name = {}
for m in json_data:
member = RecordMember(Name(m['name']),
types[m['type']],
m.get('annotation', 'value'),
optional=m.get('optional', False),
is_return_value=m.get('is_return_value', False),
default_value=m.get('default', None),
skip_serialize=m.get('skip_serialize', False))
handle_type = m.get('handle_type')
if handle_type:
member.set_handle_type(types[handle_type])
members.append(member)
members_by_name[member.name.canonical_case()] = member
for (member, m) in zip(members, json_data):
if member.annotation != 'value':
if not 'length' in m:
if member.type.category != 'object':
member.length = "constant"
member.constant_length = 1
else:
assert False
elif m['length'] == 'strlen':
member.length = 'strlen'
else:
member.length = members_by_name[m['length']]
return members
############################################################
# PARSE
############################################################
def link_object(obj, types):
def make_method(json_data):
arguments = linked_record_members(json_data.get('args', []), types)
return Method(Name(json_data['name']),
types[json_data.get('returns', 'void')], arguments)
obj.methods = [make_method(m) for m in obj.json_data.get('methods', [])]
obj.methods.sort(key=lambda method: method.name.canonical_case())
def link_structure(struct, types):
struct.members = linked_record_members(struct.json_data['members'], types)
def link_callback(callback, types):
callback.arguments = linked_record_members(callback.json_data['args'],
types)
def link_typedef(typedef, types):
typedef.type = types[typedef.json_data['type']]
# Sort structures so that if struct A has struct B as a member, then B is
# listed before A.
#
# This is a form of topological sort where we try to keep the order reasonably
# similar to the original order (though the sort isn't technically stable).
#
# It works by computing for each struct type what is the depth of its DAG of
# dependents, then resorting based on that depth using Python's stable sort.
# This makes a toposort because if A depends on B then its depth will be bigger
# than B's. It is also nice because all nodes with the same depth are kept in
# the input order.
def topo_sort_structure(structs):
for struct in structs:
struct.visited = False
struct.subdag_depth = 0
def compute_depth(struct):
if struct.visited:
return struct.subdag_depth
max_dependent_depth = 0
for member in struct.members:
if member.type.category == 'structure':
max_dependent_depth = max(max_dependent_depth,
compute_depth(member.type) + 1)
struct.subdag_depth = max_dependent_depth
struct.visited = True
return struct.subdag_depth
for struct in structs:
compute_depth(struct)
result = sorted(structs, key=lambda struct: struct.subdag_depth)
for struct in structs:
del struct.visited
del struct.subdag_depth
return result
def parse_json(json):
category_to_parser = {
'bitmask': BitmaskType,
'enum': EnumType,
'native': NativeType,
'callback': CallbackType,
'object': ObjectType,
'structure': StructureType,
'typedef': TypedefType,
}
types = {}
by_category = {}
for name in category_to_parser.keys():
by_category[name] = []
for (name, json_data) in json.items():
if name[0] == '_':
continue
category = json_data['category']
parsed = category_to_parser[category](name, json_data)
by_category[category].append(parsed)
types[name] = parsed
for obj in by_category['object']:
link_object(obj, types)
for struct in by_category['structure']:
link_structure(struct, types)
for callback in by_category['callback']:
link_callback(callback, types)
for typedef in by_category['typedef']:
link_typedef(typedef, types)
for category in by_category.keys():
by_category[category] = sorted(
by_category[category], key=lambda typ: typ.name.canonical_case())
by_category['structure'] = topo_sort_structure(by_category['structure'])
for struct in by_category['structure']:
struct.update_metadata()
return {'types': types, 'by_category': by_category}
############################################################
# WIRE STUFF
############################################################
# Create wire commands from api methods
def compute_wire_params(api_params, wire_json):
wire_params = api_params.copy()
types = wire_params['types']
commands = []
return_commands = []
wire_json['special items']['client_handwritten_commands'] += wire_json[
'special items']['client_side_commands']
# Generate commands from object methods
for api_object in wire_params['by_category']['object']:
for method in api_object.methods:
command_name = concat_names(api_object.name, method.name)
command_suffix = Name(command_name).CamelCase()
# Only object return values or void are supported.
# Other methods must be handwritten.
is_object = method.return_type.category == 'object'
is_void = method.return_type.name.canonical_case() == 'void'
if not (is_object or is_void):
assert command_suffix in (
wire_json['special items']['client_handwritten_commands'])
continue
if command_suffix in (
wire_json['special items']['client_side_commands']):
continue
# Create object method commands by prepending "self"
members = [
RecordMember(Name('self'), types[api_object.dict_name],
'value')
]
members += method.arguments
# Client->Server commands that return an object return the
# result object handle
if method.return_type.category == 'object':
result = RecordMember(Name('result'),
types['ObjectHandle'],
'value',
is_return_value=True)
result.set_handle_type(method.return_type)
members.append(result)
command = Command(command_name, members)
command.derived_object = api_object
command.derived_method = method
commands.append(command)
for (name, json_data) in wire_json['commands'].items():
commands.append(Command(name, linked_record_members(json_data, types)))
for (name, json_data) in wire_json['return commands'].items():
return_commands.append(
Command(name, linked_record_members(json_data, types)))
wire_params['cmd_records'] = {
'command': commands,
'return command': return_commands
}
for commands in wire_params['cmd_records'].values():
for command in commands:
command.update_metadata()
commands.sort(key=lambda c: c.name.canonical_case())
wire_params.update(wire_json.get('special items', {}))
return wire_params
#############################################################
# Generator
#############################################################
def as_varName(*names):
return names[0].camelCase() + ''.join(
[name.CamelCase() for name in names[1:]])
def as_cType(name):
if name.native:
return name.concatcase()
else:
return 'WGPU' + name.CamelCase()
def as_cTypeDawn(name):
if name.native:
return name.concatcase()
else:
return 'Dawn' + name.CamelCase()
def as_cTypeEnumSpecialCase(typ):
if typ.category == 'bitmask':
return as_cType(typ.name) + 'Flags'
return as_cType(typ.name)
def as_cppType(name):
if name.native:
return name.concatcase()
else:
return name.CamelCase()
def as_jsEnumValue(value):
if value.jsrepr: return value.jsrepr
return "'" + value.name.js_enum_case() + "'"
def convert_cType_to_cppType(typ, annotation, arg, indent=0):
if typ.category == 'native':
return arg
if annotation == 'value':
if typ.category == 'object':
return '{}::Acquire({})'.format(as_cppType(typ.name), arg)
elif typ.category == 'structure':
converted_members = [
convert_cType_to_cppType(
member.type, member.annotation,
'{}.{}'.format(arg, as_varName(member.name)), indent + 1)
for member in typ.members
]
converted_members = [(' ' * 4) + m for m in converted_members]
converted_members = ',\n'.join(converted_members)
return as_cppType(typ.name) + ' {\n' + converted_members + '\n}'
else:
return 'static_cast<{}>({})'.format(as_cppType(typ.name), arg)
else:
return 'reinterpret_cast<{} {}>({})'.format(as_cppType(typ.name),
annotation, arg)
def decorate(name, typ, arg):
if arg.annotation == 'value':
return typ + ' ' + name
elif arg.annotation == '*':
return typ + ' * ' + name
elif arg.annotation == 'const*':
return typ + ' const * ' + name
elif arg.annotation == 'const*const*':
return 'const ' + typ + '* const * ' + name
else:
assert False
def annotated(typ, arg):
name = as_varName(arg.name)
return decorate(name, typ, arg)
def as_cEnum(type_name, value_name):
assert not type_name.native and not value_name.native
return 'WGPU' + type_name.CamelCase() + '_' + value_name.CamelCase()
def as_cEnumDawn(type_name, value_name):
assert not type_name.native and not value_name.native
return ('DAWN' + '_' + type_name.SNAKE_CASE() + '_' +
value_name.SNAKE_CASE())
def as_cppEnum(value_name):
assert not value_name.native
if value_name.concatcase()[0].isdigit():
return "e" + value_name.CamelCase()
return value_name.CamelCase()
def as_cMethod(type_name, method_name):
assert not type_name.native and not method_name.native
return 'wgpu' + type_name.CamelCase() + method_name.CamelCase()
def as_cMethodDawn(type_name, method_name):
assert not type_name.native and not method_name.native
return 'dawn' + type_name.CamelCase() + method_name.CamelCase()
def as_MethodSuffix(type_name, method_name):
assert not type_name.native and not method_name.native
return type_name.CamelCase() + method_name.CamelCase()
def as_cProc(type_name, method_name):
assert not type_name.native and not method_name.native
return 'WGPU' + 'Proc' + type_name.CamelCase() + method_name.CamelCase()
def as_cProcDawn(type_name, method_name):
assert not type_name.native and not method_name.native
return 'Dawn' + 'Proc' + type_name.CamelCase() + method_name.CamelCase()
def as_frontendType(typ):
if typ.category == 'object':
return typ.name.CamelCase() + 'Base*'
elif typ.category in ['bitmask', 'enum']:
return 'wgpu::' + typ.name.CamelCase()
elif typ.category == 'structure':
return as_cppType(typ.name)
else:
return as_cType(typ.name)
def as_wireType(typ):
if typ.category == 'object':
return typ.name.CamelCase() + '*'
elif typ.category in ['bitmask', 'enum', 'structure']:
return 'WGPU' + typ.name.CamelCase()
else:
return as_cppType(typ.name)
def c_methods(types, typ):
return typ.methods + [
Method(Name('reference'), types['void'], []),
Method(Name('release'), types['void'], []),
]
def get_c_methods_sorted_by_name(api_params):
unsorted = [(as_MethodSuffix(typ.name, method.name), typ, method) \
for typ in api_params['by_category']['object'] \
for method in c_methods(api_params['types'], typ) ]
return [(typ, method) for (_, typ, method) in sorted(unsorted)]
def has_callback_arguments(method):
return any(arg.type.category == 'callback' for arg in method.arguments)
class MultiGeneratorFromDawnJSON(Generator):
def get_description(self):
return 'Generates code for various target from Dawn.json.'
def add_commandline_arguments(self, parser):
allowed_targets = [
'dawn_headers', 'dawncpp_headers', 'dawncpp', 'dawn_proc',
'mock_webgpu', 'dawn_wire', "dawn_native_utils"
]
parser.add_argument('--dawn-json',
required=True,
type=str,
help='The DAWN JSON definition to use.')
parser.add_argument('--wire-json',
default=None,
type=str,
help='The DAWN WIRE JSON definition to use.')
parser.add_argument(
'--targets',
required=True,
type=str,
help=
'Comma-separated subset of targets to output. Available targets: '
+ ', '.join(allowed_targets))
def get_file_renders(self, args):
with open(args.dawn_json) as f:
loaded_json = json.loads(f.read())
api_params = parse_json(loaded_json)
targets = args.targets.split(',')
wire_json = None
if args.wire_json:
with open(args.wire_json) as f:
wire_json = json.loads(f.read())
base_params = {
'Name': lambda name: Name(name),
'as_annotated_cType': \
lambda arg: annotated(as_cTypeEnumSpecialCase(arg.type), arg),
'as_annotated_cppType': \
lambda arg: annotated(as_cppType(arg.type.name), arg),
'as_cEnum': as_cEnum,
'as_cEnumDawn': as_cEnumDawn,
'as_cppEnum': as_cppEnum,
'as_cMethod': as_cMethod,
'as_cMethodDawn': as_cMethodDawn,
'as_MethodSuffix': as_MethodSuffix,
'as_cProc': as_cProc,
'as_cProcDawn': as_cProcDawn,
'as_cType': as_cType,
'as_cTypeDawn': as_cTypeDawn,
'as_cppType': as_cppType,
'as_jsEnumValue': as_jsEnumValue,
'convert_cType_to_cppType': convert_cType_to_cppType,
'as_varName': as_varName,
'decorate': decorate,
'c_methods': lambda typ: c_methods(api_params['types'], typ),
'c_methods_sorted_by_name': \
get_c_methods_sorted_by_name(api_params),
}
renders = []
if 'dawn_headers' in targets:
renders.append(
FileRender('webgpu.h', 'src/include/dawn/webgpu.h',
[base_params, api_params]))
renders.append(
FileRender('dawn_proc_table.h',
'src/include/dawn/dawn_proc_table.h',
[base_params, api_params]))
if 'dawncpp_headers' in targets:
renders.append(
FileRender('webgpu_cpp.h', 'src/include/dawn/webgpu_cpp.h',
[base_params, api_params]))
renders.append(
FileRender('webgpu_cpp_print.h',
'src/include/dawn/webgpu_cpp_print.h',
[base_params, api_params]))
if 'dawn_proc' in targets:
renders.append(
FileRender('dawn_proc.c', 'src/dawn/dawn_proc.c',
[base_params, api_params]))
renders.append(
FileRender('dawn_thread_dispatch_proc.cpp',
'src/dawn/dawn_thread_dispatch_proc.cpp',
[base_params, api_params]))
if 'dawncpp' in targets:
renders.append(
FileRender('webgpu_cpp.cpp', 'src/dawn/webgpu_cpp.cpp',
[base_params, api_params]))
if 'emscripten_bits' in targets:
renders.append(
FileRender('webgpu_struct_info.json',
'src/dawn/webgpu_struct_info.json',
[base_params, api_params]))
renders.append(
FileRender('library_webgpu_enum_tables.js',
'src/dawn/library_webgpu_enum_tables.js',
[base_params, api_params]))
if 'mock_webgpu' in targets:
mock_params = [
base_params, api_params, {
'has_callback_arguments': has_callback_arguments
}
]
renders.append(
FileRender('mock_webgpu.h', 'src/dawn/mock_webgpu.h',
mock_params))
renders.append(
FileRender('mock_webgpu.cpp', 'src/dawn/mock_webgpu.cpp',
mock_params))
if 'dawn_native_utils' in targets:
frontend_params = [
base_params,
api_params,
{
# TODO: as_frontendType and co. take a Type, not a Name :(
'as_frontendType': lambda typ: as_frontendType(typ),
'as_annotated_frontendType': \
lambda arg: annotated(as_frontendType(arg.type), arg),
}
]
renders.append(
FileRender('dawn_native/ValidationUtils.h',
'src/dawn_native/ValidationUtils_autogen.h',
frontend_params))
renders.append(
FileRender('dawn_native/ValidationUtils.cpp',
'src/dawn_native/ValidationUtils_autogen.cpp',
frontend_params))
renders.append(
FileRender('dawn_native/wgpu_structs.h',
'src/dawn_native/wgpu_structs_autogen.h',
frontend_params))
renders.append(
FileRender('dawn_native/wgpu_structs.cpp',
'src/dawn_native/wgpu_structs_autogen.cpp',
frontend_params))
renders.append(
FileRender('dawn_native/ProcTable.cpp',
'src/dawn_native/ProcTable.cpp', frontend_params))
renders.append(
FileRender('dawn_native/ChainUtils.h',
'src/dawn_native/ChainUtils_autogen.h',
frontend_params))
renders.append(
FileRender('dawn_native/ChainUtils.cpp',
'src/dawn_native/ChainUtils_autogen.cpp',
frontend_params))
if 'dawn_wire' in targets:
additional_params = compute_wire_params(api_params, wire_json)
wire_params = [
base_params, api_params, {
'as_wireType': as_wireType,
'as_annotated_wireType': \
lambda arg: annotated(as_wireType(arg.type), arg),
}, additional_params
]
renders.append(
FileRender('dawn_wire/ObjectType.h',
'src/dawn_wire/ObjectType_autogen.h', wire_params))
renders.append(
FileRender('dawn_wire/WireCmd.h',
'src/dawn_wire/WireCmd_autogen.h', wire_params))
renders.append(
FileRender('dawn_wire/WireCmd.cpp',
'src/dawn_wire/WireCmd_autogen.cpp', wire_params))
renders.append(
FileRender('dawn_wire/client/ApiObjects.h',
'src/dawn_wire/client/ApiObjects_autogen.h',
wire_params))
renders.append(
FileRender('dawn_wire/client/ApiProcs.cpp',
'src/dawn_wire/client/ApiProcs_autogen.cpp',
wire_params))
renders.append(
FileRender('dawn_wire/client/ClientBase.h',
'src/dawn_wire/client/ClientBase_autogen.h',
wire_params))
renders.append(
FileRender('dawn_wire/client/ClientHandlers.cpp',
'src/dawn_wire/client/ClientHandlers_autogen.cpp',
wire_params))
renders.append(
FileRender(
'dawn_wire/client/ClientPrototypes.inc',
'src/dawn_wire/client/ClientPrototypes_autogen.inc',
wire_params))
renders.append(
FileRender('dawn_wire/server/ServerBase.h',
'src/dawn_wire/server/ServerBase_autogen.h',
wire_params))
renders.append(
FileRender('dawn_wire/server/ServerDoers.cpp',
'src/dawn_wire/server/ServerDoers_autogen.cpp',
wire_params))
renders.append(
FileRender('dawn_wire/server/ServerHandlers.cpp',
'src/dawn_wire/server/ServerHandlers_autogen.cpp',
wire_params))
renders.append(
FileRender(
'dawn_wire/server/ServerPrototypes.inc',
'src/dawn_wire/server/ServerPrototypes_autogen.inc',
wire_params))
return renders
def get_dependencies(self, args):
deps = [os.path.abspath(args.dawn_json)]
if args.wire_json != None:
deps += [os.path.abspath(args.wire_json)]
return deps
if __name__ == '__main__':
sys.exit(run_generator(MultiGeneratorFromDawnJSON()))