Skip to content

Commit

Permalink
Merge branch 'main' into mdi_count
Browse files Browse the repository at this point in the history
  • Loading branch information
fyellin committed Oct 1, 2024
2 parents 7f3cf38 + b73abc3 commit a91ca7e
Show file tree
Hide file tree
Showing 35 changed files with 1,674 additions and 928 deletions.
109 changes: 76 additions & 33 deletions codegen/apipatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,32 @@
"""

from codegen.utils import print, format_code, to_snake_case, to_camel_case, Patcher
from codegen.idlparser import get_idl_parser
from codegen.idlparser import get_idl_parser, Attribute
from codegen.files import file_cache


# In wgpu-py, we make some args optional, that are not optional in the
# IDL. Reasons may be because it makes sense to be able to omit them,
# or because the WebGPU says its optional while IDL says its not, or
# for backwards compatibility. These args have a default value of
# 'optional' (which is just None) so we can recognise them. If IDL
# makes any of these args optional, their presense in this list is
# ignored.
ARGS_TO_MAKE_OPTIONAL = {
("compilation_hints", "compilation_hints"), # idl actually has a default
("create_shader_module", "source_map"),
("begin_compute_pass", "timestamp_writes"),
("begin_render_pass", "timestamp_writes"),
("begin_render_pass", "depth_stencil_attachment"),
("begin_render_pass", "occlusion_query_set"),
("create_render_pipeline", "depth_stencil"),
("create_render_pipeline", "fragment"),
("create_render_pipeline_async", "depth_stencil"),
("create_render_pipeline_async", "fragment"),
("create_render_bundle_encoder", "depth_stencil_format"),
}


def patch_base_api(code):
"""Given the Python code, applies patches to make the code conform
to the IDL.
Expand Down Expand Up @@ -406,49 +428,57 @@ def get_method_def(self, classname, methodname):
# Get arg names and types
idl_line = functions[name_idl]
args = idl_line.split("(", 1)[1].split(")", 1)[0].split(",")
args = [arg.strip() for arg in args if arg.strip()]
raw_defaults = [arg.partition("=")[2].strip() for arg in args]
place_holder_default = False
defaults = []
for default, arg in zip(raw_defaults, args):
if default:
place_holder_default = "None" # any next args must have a default
elif arg.startswith("optional "):
default = "None"
else:
default = place_holder_default
defaults.append(default)

argnames = [arg.split("=")[0].split()[-1] for arg in args]
argnames = [to_snake_case(argname) for argname in argnames]
argnames = [(f"{n}={v}" if v else n) for n, v in zip(argnames, defaults)]
argtypes = [arg.split("=")[0].split()[-2] for arg in args]
args = [Attribute(arg) for arg in args if arg.strip()]

# If one arg that is a dict, flatten dict to kwargs
if len(argtypes) == 1 and argtypes[0].endswith(
if len(args) == 1 and args[0].typename.endswith(
("Options", "Descriptor", "Configuration")
):
assert argtypes[0].startswith("GPU")
fields = self.idl.structs[argtypes[0][3:]].values() # struct fields
py_args = [self._arg_from_struct_field(field) for field in fields]
assert args[0].typename.startswith("GPU")
des_is_optional = bool(args[0].default)
attributes = self.idl.structs[args[0].typename[3:]].values()
py_args = [
self._arg_from_attribute(methodname, attr, des_is_optional)
for attr in attributes
]
if py_args[0].startswith("label: str"):
py_args[0] = 'label=""'
py_args[0] = 'label: str=""'
py_args = ["self", "*", *py_args]
else:
py_args = ["self", *argnames]
py_args = [self._arg_from_attribute(methodname, attr) for attr in args]
py_args = ["self", *py_args]

# IDL has some signatures that cannot work in Python. This may be a bug in idl
known_bugged_methods = {"GPUPipelineError.__init__"}
remove_default = False
for i in reversed(range(len(py_args))):
arg = py_args[i]
if "=" in arg:
if remove_default:
py_args[i] = arg.split("=")[0]
assert f"{classname}.{methodname}" in known_bugged_methods
else:
remove_default = True

# Construct final def
line = preamble + ", ".join(py_args) + "): pass\n"
line = format_code(line, True).split("):")[0] + "):"
return " " + line

def _arg_from_struct_field(self, field):
name = to_snake_case(field.name)
d = field.default
t = self.idl.resolve_type(field.typename)
def _arg_from_attribute(self, methodname, attribute, force_optional=False):
name = to_snake_case(attribute.name)
optional_in_py = (methodname, name) in ARGS_TO_MAKE_OPTIONAL
d = attribute.default
t = self.idl.resolve_type(attribute.typename)
result = name
if (force_optional or optional_in_py) and not d:
d = "optional"
if t:
result += f": {t}"
# If default is None, the type won't match, so we need to mark it optional
if d == "None":
result += f": Optional[{t}]"
else:
result += f": {t}"
if d:
d = {"false": "False", "true": "True"}.get(d, d)
result += f"={d}"
Expand Down Expand Up @@ -504,7 +534,20 @@ def get_method_comment(self, classname, methodname):
functions = self.idl.classes[classname].functions
name_idl = self.method_is_known(classname, methodname)
if name_idl:
return " # IDL: " + functions[name_idl]
idl_line = functions[name_idl]

args = idl_line.split("(", 1)[1].split(")", 1)[0].split(",")
args = [Attribute(arg) for arg in args if arg.strip()]

# If one arg that is a dict, flatten dict to kwargs
if len(args) == 1 and args[0].typename.endswith(
("Options", "Descriptor", "Configuration")
):
assert args[0].typename.startswith("GPU")
attributes = self.idl.structs[args[0].typename[3:]].values()
idl_line += " -> " + ", ".join(attr.line for attr in attributes)

return " # IDL: " + idl_line


class BackendApiPatcher(AbstractApiPatcher):
Expand Down Expand Up @@ -575,7 +618,7 @@ def apply(self, code):

idl = get_idl_parser()
all_structs = set()
ignore_structs = {"Extent3D"}
ignore_structs = {"Extent3D", "Origin3D"}

for classname, i1, i2 in self.iter_classes():
if classname not in idl.classes:
Expand Down Expand Up @@ -621,8 +664,8 @@ def apply(self, code):

def _get_sub_structs(self, idl, structname):
structnames = {structname}
for structfield in idl.structs[structname].values():
structname2 = structfield.typename[3:] # remove "GPU"
for attribute in idl.structs[structname].values():
structname2 = attribute.typename[3:] # remove "GPU"
if structname2 in idl.structs:
structnames.update(self._get_sub_structs(idl, structname2))
return structnames
2 changes: 1 addition & 1 deletion codegen/apiwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def write_structs():
# Object-docstring as a comment
for field in d.values():
tp = idl.resolve_type(field.typename).strip("'")
if field.default is not None:
if field.default:
pylines.append(
resolve_crossrefs(f"#: * {field.name} :: {tp} = {field.default}")
)
Expand Down
64 changes: 35 additions & 29 deletions codegen/idlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,32 @@ def get_idl_parser(*, allow_cache=True):
return idl


class StructField:
"""A little object to specify the field of a struct."""

def __init__(self, line, name, typename, default=None):
self.line = line
self.name = name
self.typename = typename
class Attribute:
"""A little object to hold a function argument or struct field."""

def __init__(self, line):
self.line = line.strip().strip(",;").strip()

default = None # None means 'no default' and "None" kinda means "auto".
arg = self.line
if "=" in arg:
arg, default = arg.rsplit("=", 1)
arg, default = arg.strip(), default.strip()
arg_type, arg_name = arg.strip().rsplit(" ", 1)
if arg_type.startswith("required "):
arg_type = arg_type.split(" ", 1)[1]
# required args should not have a default
assert default is None
elif arg_type.startswith("optional "):
arg_type = arg_type.split(" ", 1)[1]
default = default or "None"

self.name = arg_name
self.typename = arg_type
self.default = default

def __repr__(self):
return f"<StructField '{self.typename} {self.name}'>"
return f"<Attribute '{self.typename} {self.name}'>"

def to_str(self):
return self.line
Expand All @@ -65,7 +80,7 @@ class IdlParser:
* flags: a dict mapping the (neutral) flag name to a dict of field-value pairs.
* enums: a dict mapping the (Pythonic) enum name to a dict of field-value pairs.
* structs: a dict mapping the (Pythonic) struct name to a dict of StructField
* structs: a dict mapping the (Pythonic) struct name to a dict of Attribute
objects.
* classes: a dict mapping the (normalized) class name an Interface object.
Expand Down Expand Up @@ -199,6 +214,7 @@ def resolve_type(self, typename):
"ImageBitmap": "memoryview",
"ImageData": "memoryview",
"VideoFrame": "memoryview",
"AllowSharedBufferSource": "memoryview",
"GPUPipelineConstantValue": "float",
"GPUExternalTexture": "object",
}
Expand All @@ -208,22 +224,22 @@ def resolve_type(self, typename):
if name.startswith("sequence<") and name.endswith(">"):
name = name.split("<")[-1].rstrip(">")
name = self.resolve_type(name).strip("'")
return f"'List[{name}]'"
return f"List[{name}]"
elif name.startswith("record<") and name.endswith(">"):
name = name.split("<")[-1].rstrip(">")
names = [self.resolve_type(t).strip("'") for t in name.split(",")]
return f"'Dict[{', '.join(names)}]'"
return f"Dict[{', '.join(names)}]"
elif " or " in name:
name = name.strip("()")
names = [self.resolve_type(t).strip("'") for t in name.split(" or ")]
names = sorted(set(names))
return f"'Union[{', '.join(names)}]'"
return f"Union[{', '.join(names)}]"

# Triage
if name in __builtins__:
return name # ok
elif name in self.classes:
return f"'{name}'" # ok, but wrap in string because can be declared later
return name
elif name.startswith("HTML"):
return "object" # anything, we ignore this stuff anyway
elif name in ["OffscreenCanvas"]:
Expand All @@ -235,11 +251,11 @@ def resolve_type(self, typename):
name = name[3:]
name = name[:-4] if name.endswith("Dict") else name
if name in self.flags:
return f"'flags.{name}'"
return f"flags.{name}"
elif name in self.enums:
return f"'enums.{name}'"
return f"enums.{name}"
elif name in self.structs:
return f"'structs.{name}'"
return f"structs.{name}"
else:
# When this happens, update the code above or the pythonmap
raise RuntimeError("Encountered unknown IDL type: ", name)
Expand Down Expand Up @@ -373,19 +389,9 @@ def _parse(self):
if not line:
continue
assert line.endswith(";")
arg = line.strip().strip(",;").strip()
default = None
if "=" in arg:
arg, default = arg.rsplit("=", 1)
arg, default = arg.strip(), default.strip()
arg_type, arg_name = arg.strip().rsplit(" ", 1)
if arg_type.startswith("required "):
arg_type = arg_type[9:]
# required args should not have a default
assert default is None
else:
default = default or "None"
d[arg_name] = StructField(line, arg_name, arg_type, default)

attribute = Attribute(line)
d[attribute.name] = attribute
self.structs[name] = d
elif line.startswith(("[Exposed=", "[Serializable]")):
pass
Expand Down
17 changes: 8 additions & 9 deletions codegen/wgpu_native_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,21 @@ def write_mappings():

# Write a few native-only mappings: key => int
pylines.append("enum_str2int = {")
for name in ["BackendType"]:
for name, use_snake in (
("BackendType", False),
("NativeFeature", True),
("PipelineStatisticName", True),
):
pylines.append(f' "{name}":' + " {")
for key, val in hp.enums[name].items():
if key == "Force32":
continue
if use_snake:
key = to_snake_case(key).replace("_", "-")
pylines.append(f' "{key}": {val},')
pylines.append(" },")
for name in ["NativeFeature"]:
pylines.append(f' "{name}":' + " {")
for key, val in hp.enums[name].items():
if key == "Force32":
continue
xkey = to_snake_case(key).replace("_", "-")
pylines.append(f' "{xkey}": {val},')
pylines.append(" },")
pylines.append("}")
pylines.append("")

# Write a few native-only mappings: int => key
# If possible, resolve to WebGPU names, otherwise use the native name.
Expand Down
59 changes: 58 additions & 1 deletion docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ It also works out of the box, because the wgpu-native DLL is shipped with wgpu-p
The wgpu_native backend provides a few extra functionalities:

.. py:function:: wgpu.backends.wgpu_native.request_device_sync(adapter, trace_path, *, label="", required_features, required_limits, default_queue)
An alternative to :func:`wgpu.GPUAdapter.request_adapter`, that streams a trace
of all low level calls to disk, so the visualization can be replayed (also on other systems),
investigated, and debugged.
Expand Down Expand Up @@ -236,6 +235,64 @@ an unsigned 32-bit integer. The ``count`` is the minimum of this value and ``max
Must be a multiple of 4.
:param max_count: The maximum number of draw operations to perform.

Some GPUs allow you collect statistics on their pipelines. Those GPUs that support this
have the feature "pipeline-statistics-query", and you must enable this feature when
getting the device.

You create a query set using the function
``wgpu.backends.wgpu_native.create_statistics_query_set``.

The possible statistics are:

* ``PipelineStatisticName.VertexShaderInvocations`` = "vertex-shader-invocations"
* The number of times the vertex shader is called.
* ``PipelineStatisticName.ClipperInvocations`` = "clipper-invocations"
* The number of triangles generated by the vertex shader.
* ``PipelineStatisticName.ClipperPrimitivesOut`` = "clipper-primitives-out"
* The number of primitives output by the clipper.
* ``PipelineStatisticName.FragmentShaderInvocations`` = "fragment-shader-invocations"
* The number of times the fragment shader is called.
* ``PipelineStatisticName.ComputeShaderInvocations`` = "compute-shader-invocations"
* The number of times the compute shader is called.

The statistics argument is a list or a tuple of statistics names. Each element of the
sequence must either be:

* The enumeration, e.g. ``PipelineStatisticName.FragmentShaderInvocations``
* A camel case string, e.g. ``"VertexShaderInvocations"``
* A snake-case string, e.g. ``"vertex-shader-invocations"``
* An underscored string, e.g. ``"vertex_shader_invocations"``

You may use any number of these statistics in a query set. Each result is an 8-byte
unsigned integer, and the total size of each entry in the query set is 8 times
the number of statistics chosen.

The statistics are always output to the query set in the order above, even if they are
given in a different order in the list.

.. py:function:: wgpu.backends.wgpu_native.create_statistics_query_set(device, count, statistics):
Create a query set that could hold count entries for the specified statistics.
The statistics are specified as a list of strings.

:param device: The device.
:param count: Number of entries that go into the query set.
:param statistics: A sequence of strings giving the desired statistics.

.. py:function:: wgpu.backends.wgpu_native.begin_pipeline_statistics_query(encoder, query_set, index):
Start collecting statistics.

:param encoder: The ComputePassEncoder or RenderPassEncoder.
:param query_set: The query set into which to save the result.
:param index: The index of the query set into which to write the result.

.. py:function:: wgpu.backends.wgpu_native.begin_pipeline_statistics_query(encoder, query_set, index):
Stop collecting statistics and write them into the query set.

:param encoder: The ComputePassEncoder or RenderPassEncoder.


The js_webgpu backend
---------------------
Expand Down
Loading

0 comments on commit a91ca7e

Please sign in to comment.