[WebGPU] Adds the @ExperimentalWebGpuApi opt-in annotation to the Kotlin WebGPU wrapper. Key changes: - Generates the `ExperimentalWebGpuApi.kt` annotation file. - Updates generator templates to automatically annotate classes, methods, or properties tagged as experimental. - Adds secondary constructor for classes with experimental members - Removes @JVMOverloads and introduces builder pattern - Add experimental api annotation to test cases Bug: b/513132857 Change-Id: Ia7762e2e503477fc7217b3700c71e14cea28263a Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/312615 Reviewed-by: Tarun Saini <sainitarun@google.com> Reviewed-by: Loko Kung <lokokung@google.com> Commit-Queue: Mridul Goyal <mridulgoyal@google.com>
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py index 723fec4e..7594140 100644 --- a/generator/dawn_json_generator.py +++ b/generator/dawn_json_generator.py
@@ -1221,6 +1221,7 @@ params_kotlin['kotlin_default'] = kotlin_default params_kotlin['kotlin_return'] = kotlin_return params_kotlin['kotlin_name'] = kotlin_name + params_kotlin['customize_structures'] = customize_structures params_kotlin['include_method'] = include_method params_kotlin['include_structure'] = include_structure params_kotlin['include_enum'] = include_enum
diff --git a/generator/templates/art/api_kotlin_enum.kt b/generator/templates/art/api_kotlin_enum.kt index a7dbfff..1453048 100644 --- a/generator/templates/art/api_kotlin_enum.kt +++ b/generator/templates/art/api_kotlin_enum.kt
@@ -14,7 +14,7 @@ * limitations under the License. */ -{% from 'art/api_kotlin_types.kt' import generate_simple_kdoc, add_kdoc_disclaimer with context %} +{% from 'art/api_kotlin_types.kt' import generate_simple_kdoc, add_kdoc_disclaimer, kotlin_member_optin, kotlin_class_optin, item_requires_optin with context %} {{ add_kdoc_disclaimer() }} package {{ kotlin_package }} @@ -32,8 +32,14 @@ {% if docstring %} {{ generate_simple_kdoc(docstring) }} {% endif %} +{% set experimental = kotlin_class_optin(enum) -%} +{% if experimental %} + //* Injects experimental opt-in requirement if the enum itself is experimental. + {{ experimental }} +{% endif %} public class {{ enum.name.CamelCase() }} private constructor() { + {% set ns = namespace(has_experimental=false) %} public companion object { //* Generating KDocs {% set enum_doc = file_docs.get(enum.name.get(), {}) %} @@ -43,6 +49,11 @@ {{ generate_simple_kdoc(value_docstring, indent_prefix = " ", line_wrap_prefix = "\n * ") }} {% endif %} + {% set optin = kotlin_member_optin(value, parent=enum) | trim %} + {% if optin %} + {{ optin }} + {% set ns.has_experimental = true %} + {% endif %} public const val {{ as_ktName(value.name.CamelCase()) }}: Int = {{ '{:#010x}'.format(value.value) }} {% endfor %} internal val names: Map<Int, String> = mapOf( @@ -59,6 +70,9 @@ {% if enum.allowConflict %} @Suppress("UniqueConstants") //* Required for duplicate constant values for compatibility. {% endif %} + {% if ns.has_experimental %} + @OptIn(ExperimentalWebGpuApi::class) + {% endif %} @IntDef( {% if enum.category == 'bitmask' %} flag = true,
diff --git a/generator/templates/art/api_kotlin_function_pointer.kt b/generator/templates/art/api_kotlin_function_pointer.kt index e7ee7b4..c4c783f 100644 --- a/generator/templates/art/api_kotlin_function_pointer.kt +++ b/generator/templates/art/api_kotlin_function_pointer.kt
@@ -14,7 +14,7 @@ * limitations under the License. */ -{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, add_kdoc_disclaimer with context %} +{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, add_kdoc_disclaimer, kotlin_member_optin, kotlin_class_optin, item_requires_optin with context %} {{ add_kdoc_disclaimer() }} package {{ kotlin_package }} @@ -27,6 +27,11 @@ {% set arg_docs_map = funtion_pointer_info.args if funtion_pointer_info else {} %} {% set function_pointer_args = function_pointer.arguments | list %} +{% set experimental = kotlin_class_optin(function_pointer) -%} +{% if experimental %} + //* Requires opt-in if this functional interface (callback) is experimental. + {{ experimental }} +{% endif %} public fun interface {{ function_pointer.name.CamelCase() }} { {% if check_if_doc_present(main_doc, "", arg_docs_map, function_pointer_args) == 'True' %} {{ generate_kdoc(main_doc, return_str, arg_docs_map, function_pointer_args , indent_prefix = " ",line_wrap_prefix = "\n * ") }}
diff --git a/generator/templates/art/api_kotlin_functions.kt b/generator/templates/art/api_kotlin_functions.kt index 95c9c87..a389d84 100644 --- a/generator/templates/art/api_kotlin_functions.kt +++ b/generator/templates/art/api_kotlin_functions.kt
@@ -14,7 +14,7 @@ * limitations under the License. */ -{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, add_kdoc_disclaimer with context %} +{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, add_kdoc_disclaimer, kotlin_member_optin with context %} {{ add_kdoc_disclaimer() }} @file:JvmName("Functions") @@ -40,6 +40,11 @@ {% if function.has_default %} @JvmOverloads {% endif %} + {% set experimental = kotlin_member_optin(function) -%} + {% if experimental %} + //* Requires opt-in if the function, its return type, or any argument is experimental. + {{ experimental }} + {% endif %} @FastNative {% if function.returns and function.returns.type.name.canonical_case() == 'status' %} @Throws({{"WebGpuException::class"}})
diff --git a/generator/templates/art/api_kotlin_object.kt b/generator/templates/art/api_kotlin_object.kt index 8f215a9..ecb202a 100644 --- a/generator/templates/art/api_kotlin_object.kt +++ b/generator/templates/art/api_kotlin_object.kt
@@ -14,7 +14,7 @@ * limitations under the License. */ -{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, generate_simple_kdoc, add_kdoc_disclaimer with context %} +{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_declaration, kotlin_definition, check_if_doc_present, generate_kdoc, generate_simple_kdoc, add_kdoc_disclaimer, kotlin_member_optin, kotlin_class_optin, item_requires_optin with context %} {% from 'art/api_kotlin_async_helpers.kt' import async_wrapper, analyze_callback with context %} {{ add_kdoc_disclaimer() }} package {{ kotlin_package }} @@ -34,6 +34,11 @@ {% if doc_str | trim %} {{ generate_simple_kdoc(doc_str) }} {% endif %} +{% set experimental = kotlin_class_optin(obj) -%} +{% if experimental %} + //* Requires opt-in if this entire WebGPU object class itself is experimental. + {{ experimental }} +{% endif %} public class {{ kotlin_name(obj) }} private constructor(public val handle: Long): AutoCloseable { {% set all_method_info = object_info.methods if object_info else {} %} {% for method in obj.methods if include_method(obj, method) %} @@ -48,6 +53,11 @@ {{ generate_kdoc(main_doc, return_doc, arg_docs_map, method_args, "\n * ", indent_prefix = " ") }} {%- endif %} + {% set experimental = kotlin_member_optin(method, parent=obj) -%} + {% if experimental %} + //* Requires opt-in if the method, its return type, or any of its arguments require experimental opt-in. + {{ experimental }} + {% endif %} @FastNative @JvmName("{{ method.name.camelCase() }}") {% for arg in kotlin_record_members(method.arguments) %} @@ -75,6 +85,9 @@ //* For the Kotlin getter, strip word 'get' from name and convert the remainder to //* camelCase() (lower case first word). E.g. "get foo bar" translated to fooBar. {% set name = method.name.chunks[1] + method.name.chunks[2:] | map('title') | join %} + {% if experimental %} + {{ experimental }} + {% endif %} @get:JvmName("{{ name }}") public val {{ name }}: {{ kotlin_declaration(_kotlin_return) if _kotlin_return else 'Unit' }} get() = {{ method.name.camelCase() }}()
diff --git a/generator/templates/art/api_kotlin_structure.kt b/generator/templates/art/api_kotlin_structure.kt index d44930c..cf9278d 100644 --- a/generator/templates/art/api_kotlin_structure.kt +++ b/generator/templates/art/api_kotlin_structure.kt
@@ -14,7 +14,7 @@ * limitations under the License. */ -{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_definition, kotlin_declaration, generate_simple_kdoc, add_kdoc_disclaimer with context %} +{% from 'art/api_kotlin_types.kt' import kotlin_annotation, kotlin_definition, kotlin_declaration, generate_simple_kdoc, add_kdoc_disclaimer, kotlin_member_optin, kotlin_class_optin, item_requires_optin, item_is_experimental, kotlin_parameter, kotlin_chain_parameter, kotlin_property, kotlin_chain_property with context %} {{ add_kdoc_disclaimer() }} package {{ kotlin_package }} @@ -35,102 +35,322 @@ {%- set ns.default_count = ns.default_count + 1 -%} {%- endfor -%} -{%- set use_builder = ns.default_count > 3 -%} +{%- set struct_config = customize_structures.get(structure.name.get(), {}) -%} +{%- set graduated_batches = struct_config.get('graduated_batches', []) -%} +{%- set class_is_experimental = item_is_experimental(structure) == 'True' -%} + +{%- set stable_members = [] -%} +{%- set experimental_members = [] -%} +{%- for member in members -%} + {%- if not class_is_experimental and item_requires_optin(member, chain_children=chain_children) == 'True' -%} + {%- do experimental_members.append(member) -%} + {%- else -%} + {%- do stable_members.append(member) -%} + {%- endif -%} +{%- endfor -%} + +{%- set stable_chain_children = [] -%} +{%- set experimental_chain_children = [] -%} +{%- for child in chain_children_list -%} + {%- if not class_is_experimental and item_requires_optin(child, chain_children=chain_children) == 'True' -%} + {%- do experimental_chain_children.append(child) -%} + {%- else -%} + {%- do stable_chain_children.append(child) -%} + {%- endif -%} +{%- endfor -%} + +{%- set has_experimental = (experimental_members or experimental_chain_children) and not class_is_experimental -%} + +{%- set stable_ctor_has_defaults = namespace(val=false) -%} +{%- for member in stable_members -%} + {%- if kotlin_default(member) is not none -%} + {%- set stable_ctor_has_defaults.val = true -%} + {%- break -%} + {%- endif -%} +{%- endfor -%} +{%- if not stable_ctor_has_defaults.val -%} + {%- if stable_chain_children -%} + {%- set stable_ctor_has_defaults.val = true -%} + {%- endif -%} +{%- endif -%} //* Generating KDocs -{% set all_structs_info = kdocs.structs %} -{% set struct_info = all_structs_info.get(structure.name.get()) %} -{% set main_doc = struct_info.doc if struct_info else "" %} -{% if main_doc %} +{% set all_structs_info = kdocs.structs -%} +{%- set struct_info = all_structs_info.get(structure.name.get()) -%} +{%- set main_doc = struct_info.doc if struct_info else "" -%} +{%- if main_doc %} {{ generate_simple_kdoc(main_doc) }} {% endif %} -public class {{ kotlin_name(structure) }} - {%- if not use_builder %} - {%- for member in members %} - {% if kotlin_default(member) is not none %} @JvmOverloads constructor{% break %}{% endif %} - {%- endfor %} - {%- endif %}( - {% for member in members %} - //* Generating KDocs - {% set member_doc = struct_info.members.get(member.name.get(), "") if struct_info and struct_info.members else "" %} - {% if member_doc %} - {{ generate_simple_kdoc(member_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} - {% endif %} - {% if member.type.name.get() == "bool" %}@get:JvmName("is{{ member.name.CamelCase() }}") {% endif %}{{ kotlin_annotation(member) }} public var {{ member.name.camelCase() }}: {{ kotlin_definition(member) }}, - {% endfor %} - {% for structure in chain_children_list %} - //* Generating KDocs - {% set chain_struct_doc = all_structs_info.get(structure.name.get()).doc if all_structs_info.get(structure.name.get()) else "" %} - {% if chain_struct_doc %} - {{ generate_simple_kdoc(chain_struct_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} - {% endif %} - public var {{ structure.name.camelCase() }}: {{ kotlin_name(structure) }}? = null, - {% endfor %} -) -{% if ns.callback_count > 1 or use_builder %} - { - {% if ns.callback_count > 1 %} - {% for member in members %} - {% if member.name.camelCase().endswith('Callback') %} - {% set callback_name = member.name.camelCase() %} - {% set executor_name = callback_name + 'Executor' %} - {% set function_name = 'set' + member.name.CamelCase() %} - public fun {{ function_name }}( - {{ executor_name }}: java.util.concurrent.Executor, - {{ callback_name }}: {{ kotlin_definition(member) }} - ): Unit { - this.{{ executor_name }} = {{ executor_name }} - this.{{ callback_name }} = {{ callback_name }} - } - {% endif %} - {% endfor %} - {% endif %} - - {% if use_builder %} - /** - * Builder for [{{ kotlin_name(structure) }}]. - */ - public class Builder( - {% for member in members %} - {% if kotlin_default(member) is none %} - {{ kotlin_annotation(member) }} private val {{ member.name.camelCase() }}: {{ kotlin_declaration(member) }}{{ "," if not loop.last or ns.default_count > 0 }} +{%- set experimental = kotlin_class_optin(structure) -%} +{%- if experimental %} + {{ experimental }} +{% endif %} +//* Generates legacy constructors for Android ABI compatibility. +//* Omits newly added non-nullable fields to maintain stable APIs. +{% macro render_legacy_constructors(graduated_batches, stable_members, stable_chain_children, structure) %} + {% if graduated_batches %} + {% for batch in graduated_batches %} + {% set batch_index = loop.index0 %} + {% set current_legacy_members = [] %} + {% for m in stable_members %} + {% set m_name = m.name.get() %} + {% set in_current_or_future_batch = namespace(val=false) %} + {% for future_batch in graduated_batches[batch_index:] %} + {% if m_name in future_batch %} + {% set in_current_or_future_batch.val = true %} {% endif %} {% endfor %} + {% if not in_current_or_future_batch.val %} + {% do current_legacy_members.append(m) %} + {% endif %} + {% endfor %} + {% set current_legacy_chain_children = [] %} + {% for child in stable_chain_children %} + {% set child_name = child.name.get() %} + {% set in_current_or_future_batch = namespace(val=false) %} + {% for future_batch in graduated_batches[batch_index:] %} + {% if child_name in future_batch %} + {% set in_current_or_future_batch.val = true %} + {% endif %} + {% endfor %} + {% if not in_current_or_future_batch.val %} + {% do current_legacy_chain_children.append(child) %} + {% endif %} + {% endfor %} + + {% set legacy_params = [] %} + {% for m in current_legacy_members %} + {% do legacy_params.append(kotlin_parameter(m, parent=structure)) %} + {%- endfor %} + {% for child in current_legacy_chain_children %} + {% do legacy_params.append(kotlin_chain_parameter(child, parent=structure)) %} + {%- endfor %} + + @Deprecated( + message = "Hidden constructor for binary compatibility", + level = DeprecationLevel.HIDDEN, + ) + public constructor( + {% for param in legacy_params %} + {{ param }}{{ "," if not loop.last }} + {% endfor %} + ) : this( + {% for m in stable_members %} + {{ m.name.camelCase() }} = {{ m.name.camelCase() if m in current_legacy_members else (kotlin_default(m) if kotlin_default(m) is not none else "null") }}, + {% endfor %} + {% for child in stable_chain_children %} + {{ child.name.camelCase() }} = {{ child.name.camelCase() if child in current_legacy_chain_children else "null" }}, + {% endfor %} + ) + {%- endfor %} + {%- endif %} +{% endmacro %} +//* Generates explicit `set<CallbackName>` methods. +//* Required for Java interop when multiple callbacks exist in a struct. +{% macro render_callbacks(members, callback_count) %} + {% if callback_count > 1 %} + {% for member in members %} + {% if member.name.camelCase().endswith('Callback') %} + {% set callback_name = member.name.camelCase() %} + {% set executor_name = callback_name + 'Executor' %} + {% set function_name = 'set' + member.name.CamelCase() %} + public fun {{ function_name }}( + {{ executor_name }}: java.util.concurrent.Executor, + {{ callback_name }}: {{ kotlin_definition(member) }} + ): Unit { + this.{{ executor_name }} = {{ executor_name }} + this.{{ callback_name }} = {{ callback_name }} + } + + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} +//* Generates the Builder inner class for flexible initialization. +{% macro render_builder(structure, members, chain_children_list, default_count, include_optins, has_experimental, chain_children) %} + /** + * Builder for [{{ kotlin_name(structure) }}]. + */ + public class Builder( + {% for member in members %} + {% if kotlin_default(member) is none %} + {% set optin_str = kotlin_member_optin(member, parent=structure, chain_children=chain_children) if include_optins else "" %} + {{ kotlin_annotation(member) }}{{ " " ~ optin_str if optin_str else "" }} private val {{ member.name.camelCase() }}: {{ kotlin_declaration(member) }}{{ "," if not loop.last or default_count > 0 }} + {% endif %} + {% endfor %} + ) { + {% for member in members %} + {% if kotlin_default(member) is not none %} + {% set optin_str = kotlin_member_optin(member, parent=structure, chain_children=chain_children) if include_optins else "" %} + {{ kotlin_annotation(member) }}{{ " " ~ optin_str if optin_str else "" }} private var {{ member.name.camelCase() }}: {{ kotlin_definition(member) }} + {% endif %} + {% endfor %} + {% for child in chain_children_list %} + {% set optin_str = kotlin_member_optin(child, parent=structure, chain_children=chain_children) if include_optins else "" %} + {{ optin_str ~ " " if optin_str else "" }}private var {{ child.name.camelCase() }}: {{ kotlin_name(child) }}? = null + {% endfor %} + + {% for member in members %} + {% if kotlin_default(member) is not none %} + {% set optin_str = kotlin_member_optin(member, parent=structure, chain_children=chain_children) if include_optins else "" %} + {{ optin_str ~ " " if optin_str else "" }}public fun set{{ member.name.CamelCase() }}({{ kotlin_annotation(member) }} {{ member.name.camelCase() }}: {{ kotlin_declaration(member) }}): Builder = apply { + this.{{ member.name.camelCase() }} = {{ member.name.camelCase() }} + } + {% endif %} + {% endfor %} + {% for child in chain_children_list %} + {% set optin_str = kotlin_member_optin(child, parent=structure, chain_children=chain_children) if include_optins else "" %} + {{ optin_str ~ " " if optin_str else "" }}public fun set{{ child.name.CamelCase() }}({{ child.name.camelCase() }}: {{ kotlin_name(child) }}?): Builder = apply { + this.{{ child.name.camelCase() }} = {{ child.name.camelCase() }} + } + {% endfor %} + + /** + * Builds the [{{ kotlin_name(structure) }}]. + */ + {% if include_optins and has_experimental %} + @OptIn(ExperimentalWebGpuApi::class) + {% endif %} + public fun build(): {{ kotlin_name(structure) }} = {{ kotlin_name(structure) }}( + {% for member in members %} + {{ member.name.camelCase() }} = {{ member.name.camelCase() }}, + {% endfor %} + {% for child in chain_children_list %} + {{ child.name.camelCase() }} = {{ child.name.camelCase() }}, + {% endfor %} + ) + } +{% endmacro %} +{%- if not has_experimental -%} + public class {{ kotlin_name(structure) }}( + {% for member in members %} + //* Generating KDocs + {% set member_doc = struct_info.members.get(member.name.get(), "") if struct_info and struct_info.members else "" %} + {% if member_doc %} + {{ generate_simple_kdoc(member_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} + {% endif %} + {% if member.type.name.get() == "bool" %}{{ " " }}@get:JvmName("is{{ member.name.CamelCase() }}") {% else %}{{ " " }}{% endif %}{{ kotlin_annotation(member) }} public var {{ member.name.camelCase() }}: {{ kotlin_definition(member) }}, + {% endfor %} + {% for child in chain_children_list %} + //* Generating KDocs + {% set chain_struct_doc = all_structs_info.get(child.name.get()).doc if all_structs_info.get(child.name.get()) else "" %} + {% if chain_struct_doc %} + {{ generate_simple_kdoc(chain_struct_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} + {% endif %} + public var {{ child.name.camelCase() }}: {{ kotlin_name(child) }}? = null, + {% endfor %} +) { +{% set legacy_code = render_legacy_constructors(graduated_batches, stable_members, stable_chain_children, structure) -%} +{%- if legacy_code.strip() %} + {{ legacy_code | indent(4, True) }} +{%- endif -%} +{% set callbacks_code = render_callbacks(members, ns.callback_count) -%} +{%- if callbacks_code.strip() %} + {{ callbacks_code | indent(4, True) }} +{%- endif -%} +{% set builder_code = render_builder(structure, members, chain_children_list, ns.default_count, false, has_experimental, chain_children) -%} +{%- if builder_code.strip() %} + {{ builder_code | indent(4, True) }} +{%- endif -%} +} +{%- else -%} + public class {{ kotlin_name(structure) }} { + {% for member in members %} + //* Generating KDocs + {% set member_doc = struct_info.members.get(member.name.get(), "") if struct_info and struct_info.members else "" %} + {% if member_doc %} + {{ generate_simple_kdoc(member_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} + {% endif %} + {{ kotlin_property(member, parent=structure) }} + {% endfor %} + {% for child in chain_children_list %} + //* Generating KDocs + {% set chain_struct_doc = all_structs_info.get(child.name.get()).doc if all_structs_info.get(child.name.get()) else "" %} + {% if chain_struct_doc %} + {{ generate_simple_kdoc(chain_struct_doc, indent_prefix = " ", line_wrap_prefix = '\n * ') }} + {% endif %} + {{ kotlin_chain_property(child, parent=structure) }} + {% endfor %} + + {% set stable_params = [] -%} + {%- for member in stable_members -%} + {%- do stable_params.append(kotlin_parameter(member, parent=structure)) -%} + {%- endfor -%} + {%- for child in stable_chain_children -%} + {%- do stable_params.append(kotlin_chain_parameter(child, parent=structure)) -%} + {%- endfor -%} + + {%- set all_stable_have_defaults = namespace(val=true) -%} + {%- for member in stable_members -%} + {%- if kotlin_default(member) is none -%} + {%- set all_stable_have_defaults.val = false -%} + {%- break -%} + {%- endif -%} + {% endfor %} + {%- if all_stable_have_defaults.val %} + //* Explicit no-args constructor to maintain Java binary compatibility. + //* Kotlin does not automatically generate one for secondary constructors. + public constructor() { + {% for member in stable_members %} + this.{{ member.name.camelCase() }} = {{ kotlin_default(member) }} + {% endfor %} + {% for child in stable_chain_children %} + this.{{ child.name.camelCase() }} = null + {% endfor %} + } + {% endif %} + + public constructor( + {% for param in stable_params %} + {{ param }}{{ "," if not loop.last }} + {% endfor %} + ) { + {% for member in stable_members %} + this.{{ member.name.camelCase() }} = {{ member.name.camelCase() }} + {% endfor %} + {% for child in stable_chain_children %} + this.{{ child.name.camelCase() }} = {{ child.name.camelCase() }} + {% endfor %} + } +{% set legacy_code = render_legacy_constructors(graduated_batches, stable_members, stable_chain_children, structure) -%} +{%- if legacy_code.strip() %} + {{ legacy_code | indent(4, True) }} +{%- endif -%} + {% if experimental_members or experimental_chain_children %} + {% set exp_params = [] -%} + {%- for member in members -%} + {%- set is_exp = member in experimental_members -%} + {%- do exp_params.append(kotlin_parameter(member, parent=structure, include_default=(not is_exp))) -%} + {%- endfor -%} + {%- for child in chain_children_list -%} + {%- set is_exp = child in experimental_chain_children -%} + {%- do exp_params.append(kotlin_chain_parameter(child, parent=structure, include_default=(not is_exp))) -%} + {%- endfor %} + + // TODO: When stabilizing, fold the experimental parameters into the stable constructor. The previous stable constructor should be marked with @Deprecated(level = DeprecationLevel.HIDDEN) to maintain backward compatibility. + @ExperimentalWebGpuApi + public constructor( + {% for param in exp_params %} + {{ param }}{{ "," if not loop.last }} + {% endfor %} ) { {% for member in members %} - {% if kotlin_default(member) is not none %} - {{ kotlin_annotation(member) }} private var {{ member.name.camelCase() }}: {{ kotlin_definition(member) }} - {% endif %} + this.{{ member.name.camelCase() }} = {{ member.name.camelCase() }} {% endfor %} - {% for structure in chain_children_list %} - private var {{ structure.name.camelCase() }}: {{ kotlin_name(structure) }}? = null + {% for child in chain_children_list %} + this.{{ child.name.camelCase() }} = {{ child.name.camelCase() }} {% endfor %} - - {% for member in members %} - {% if kotlin_default(member) is not none %} - public fun set{{ member.name.CamelCase() }}({{ kotlin_annotation(member) }} {{ member.name.camelCase() }}: {{ kotlin_declaration(member) }}): Builder = apply { - this.{{ member.name.camelCase() }} = {{ member.name.camelCase() }} - } - {% endif %} - {% endfor %} - {% for structure in chain_children_list %} - public fun set{{ structure.name.CamelCase() }}({{ structure.name.camelCase() }}: {{ kotlin_name(structure) }}?): Builder = apply { - this.{{ structure.name.camelCase() }} = {{ structure.name.camelCase() }} - } - {% endfor %} - - /** - * Builds the [{{ kotlin_name(structure) }}]. - */ - public fun build(): {{ kotlin_name(structure) }} = {{ kotlin_name(structure) }}( - {% for member in members %} - {{ member.name.camelCase() }} = {{ member.name.camelCase() }}, - {% endfor %} - {% for structure in chain_children_list %} - {{ structure.name.camelCase() }} = {{ structure.name.camelCase() }}, - {% endfor %} - ) } - {% endif %} - } -{% endif %} \ No newline at end of file + {% endif %} +{% set callbacks_code = render_callbacks(members, ns.callback_count) -%} +{% if callbacks_code.strip() %} + + {{ callbacks_code | indent(8, True) }} +{%- endif -%} +{% set builder_code = render_builder(structure, members, chain_children_list, ns.default_count, true, has_experimental, chain_children) -%} +{% if builder_code.strip() %} + + {{ builder_code | indent(4, True) }} +{%- endif -%} +} +{%- endif -%} \ No newline at end of file
diff --git a/generator/templates/art/api_kotlin_types.kt b/generator/templates/art/api_kotlin_types.kt index 29ba70a..0e52d8a 100644 --- a/generator/templates/art/api_kotlin_types.kt +++ b/generator/templates/art/api_kotlin_types.kt
@@ -96,12 +96,121 @@ {% macro kotlin_annotation(arg) -%} {%- if arg != None -%} - {% set type = arg.type %} - {% if type.category in ['bitmask', 'enum'] -%} - @{{ type.name.CamelCase() -}}.Type - {% endif -%} - {% endif -%} -{% endmacro %} + {%- set type = arg.type -%} + {%- if type.category in ['bitmask', 'enum'] -%} + @{{ type.name.CamelCase() }}.Type + {%- endif -%} + {%- endif -%} +{%- endmacro %} + +//* Helper macro to recursively scan if an API element or its dependencies are experimental. +//* In Kotlin, stable types must never expose experimental components in their signatures +//* without explicit warnings. This macro recursively traverses: +//* 1. The item's direct tags (e.g. structures, objects, functions). +//* 2. Members of structures (and their nested types). +//* 3. Methods of objects (including return types and method arguments). +//* 4. Constructor or function parameters. +//* 5. Chained child structures associated with extensible structures. +{% macro item_requires_optin(item, chain_children=None) -%} + {%- set ns = namespace(needs=false) -%} + {%- if item -%} + {%- if "art_experimental" in item.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- endif -%} + {%- if not ns.needs and item.type and "art_experimental" in item.type.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- endif -%} + {%- if not ns.needs and item.members -%} + {%- for member in item.members -%} + {%- if "art_experimental" in member.json_data.get("tags", []) or "art_experimental" in member.type.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- if not ns.needs and item.methods -%} + {%- for method in item.methods -%} + {%- if "art_experimental" in method.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- if method.returns and ("art_experimental" in method.returns.json_data.get("tags", []) or "art_experimental" in method.returns.type.json_data.get("tags", [])) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- for arg in method.arguments -%} + {%- if "art_experimental" in arg.type.json_data.get("tags", []) or "art_experimental" in arg.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- if ns.needs -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- if not ns.needs and item.arguments -%} + {%- for arg in item.arguments -%} + {%- if "art_experimental" in arg.json_data.get("tags", []) or "art_experimental" in arg.type.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- if not ns.needs and item.returns -%} + {%- if "art_experimental" in item.returns.json_data.get("tags", []) or "art_experimental" in item.returns.type.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- endif -%} + {%- endif -%} + {%- if not ns.needs and chain_children and item.name.get() in chain_children -%} + {%- for child in chain_children[item.name.get()] -%} + {%- if "art_experimental" in child.json_data.get("tags", []) -%} + {%- set ns.needs = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endif -%} + {{- ns.needs -}} +{%- endmacro %} + +//* Helper macro to check if a top-level API element is directly tagged as experimental. +//* This indicates that the entire WebGPU object or type is considered unstable. +//* It also checks if any required member is experimental, forcing the class to be experimental +//* to enable the primary constructor flow and prevent initialization errors. +{% macro item_is_experimental(item) -%} + {%- set ns = namespace(val=false) -%} + {%- if item and "art_experimental" in item.json_data.get("tags", []) -%} + {%- set ns.val = true -%} + {%- elif item and item.members -%} + {%- for member in item.members -%} + {%- if item_requires_optin(member) == 'True' and kotlin_default(member) is none -%} + {%- set ns.val = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {{- ns.val -}} +{%- endmacro %} + +//* Injects @ExperimentalWebGpuApi annotation on class declarations. +//* This requires consumers to opt-in to the entire namespace of an experimental class or enum. +{% macro kotlin_class_optin(item) -%} + {%- if item_is_experimental(item) == 'True' -%} + @ExperimentalWebGpuApi + {%- endif -%} +{%- endmacro %} + +//* Injects @ExperimentalWebGpuApi annotation on methods, functions, or members. +//* Ensures consumers are warned when using stable classes that expose experimental operations. +{% macro kotlin_member_optin(item, parent=None, chain_children=None) -%} + {%- if parent and item_is_experimental(parent) == 'True' -%} + {# Containing type is already experimental; members inherit this, so skip redundant annotation #} + {%- elif item_requires_optin(item, chain_children) == 'True' -%} + @ExperimentalWebGpuApi + {%- endif -%} +{%- endmacro %} + {% macro do_all_args_have_doc(all_arg_docs, code_args) %} //* Ensure inputs are not None @@ -187,3 +296,47 @@ {{indent_prefix}} * https://issuetracker.google.com/issues/new?component=1960262 {{indent_prefix}} */ {%- endmacro -%} + +//* Generates a Kotlin constructor parameter with annotations and default values for a structure member. +{% macro kotlin_parameter(member, parent, include_default=True) -%} + {%- set annotations = [] -%} + {%- set annot = kotlin_annotation(member) | trim -%} + {%- if annot -%} + {%- do annotations.append(annot) -%} + {%- endif -%} + {%- set annotations_str = annotations | join(' ') -%} + {{- annotations_str + ' ' if annotations_str -}}{{- member.name.camelCase() -}}: {{ kotlin_definition(member) if include_default else kotlin_declaration(member) -}} +{%- endmacro -%} + +//* Generates a nullable Kotlin constructor parameter for an extensible chain child structure. +{% macro kotlin_chain_parameter(child, parent, include_default=True) -%} + {{- child.name.camelCase() -}}: {{ kotlin_name(child) }}?{{ ' = null' if include_default else '' }} +{%- endmacro -%} + +//* Generates a public var property declaration for a structure member, handling annotations and opt-ins. +{% macro kotlin_property(member, parent) -%} + {%- set annotations = [] -%} + {%- if member.type.name.get() == "bool" -%} + {%- do annotations.append('@get:JvmName("is' + member.name.CamelCase() + '")') -%} + {%- endif -%} + {%- set annot = kotlin_annotation(member) | trim -%} + {%- if annot -%} + {%- do annotations.append(annot) -%} + {%- endif -%} + {%- set optin = kotlin_member_optin(member, parent=parent, chain_children=chain_children) | trim -%} + {%- if optin -%} + {%- do annotations.append(optin) -%} + {%- endif -%} + {%- set annotations_str = annotations | join(' ') -%} + {%- set is_experimental = (item_is_experimental(parent) != 'True') and (item_requires_optin(member) == 'True') -%} + {{- annotations_str + ' ' if annotations_str -}}public var {{ member.name.camelCase() -}}: {{ kotlin_definition(member) if is_experimental else kotlin_declaration(member) -}} +{%- endmacro -%} + +//* Generates a public var property declaration for an extensible chain child structure. +{% macro kotlin_chain_property(child, parent) -%} + {%- set optin = kotlin_member_optin(child, parent=parent, chain_children=chain_children) | trim -%} + {%- set is_experimental = (item_is_experimental(parent) != 'True') and (item_requires_optin(child) == 'True') -%} + {{- optin + ' ' if optin -}}public var {{ child.name.camelCase() }}: {{ kotlin_name(child) }}?{{ ' = null' if is_experimental }} +{%- endmacro -%} + +
diff --git a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUHardwareBufferTest.kt b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUHardwareBufferTest.kt index bd03d02..e04a23b 100644 --- a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUHardwareBufferTest.kt +++ b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUHardwareBufferTest.kt
@@ -46,6 +46,7 @@ @RunWith(AndroidJUnit4::class) @MediumTest +@OptIn(ExperimentalWebGpuApi::class) class GPUHardwareBufferTest { companion object {
diff --git a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUSyncFenceTest.kt b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUSyncFenceTest.kt index a00108a..8de8114 100644 --- a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUSyncFenceTest.kt +++ b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/GPUSyncFenceTest.kt
@@ -43,6 +43,7 @@ @RunWith(AndroidJUnit4::class) @MediumTest +@OptIn(ExperimentalWebGpuApi::class) class GPUSyncFenceTest { companion object {
diff --git a/tools/android/webgpu/src/main/java/androidx/webgpu/ExperimentalWebGpuApi.kt b/tools/android/webgpu/src/main/java/androidx/webgpu/ExperimentalWebGpuApi.kt new file mode 100644 index 0000000..286eb78 --- /dev/null +++ b/tools/android/webgpu/src/main/java/androidx/webgpu/ExperimentalWebGpuApi.kt
@@ -0,0 +1,33 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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. + */ + +package androidx.webgpu + +import kotlin.RequiresOptIn +import kotlin.annotation.AnnotationRetention +import kotlin.annotation.Retention +import kotlin.annotation.Target + +@RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "This WebGPU API is experimental and subject to change.") +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.CONSTRUCTOR +) +public annotation class ExperimentalWebGpuApi
diff --git a/tools/android/webgpu/src/main/java/androidx/webgpu/helper/GPUAndroidHardwareBufferUtil.kt b/tools/android/webgpu/src/main/java/androidx/webgpu/helper/GPUAndroidHardwareBufferUtil.kt index 8b961ed..54994fc 100644 --- a/tools/android/webgpu/src/main/java/androidx/webgpu/helper/GPUAndroidHardwareBufferUtil.kt +++ b/tools/android/webgpu/src/main/java/androidx/webgpu/helper/GPUAndroidHardwareBufferUtil.kt
@@ -6,9 +6,11 @@ import androidx.webgpu.GPUDevice import androidx.webgpu.GPUExternalTextureDescriptor import androidx.webgpu.GPUHardwareBufferExternalTexture +import androidx.webgpu.ExperimentalWebGpuApi import androidx.webgpu.GPUHardwareBufferTexture @RequiresApi(Build.VERSION_CODES.O) +@OptIn(ExperimentalWebGpuApi::class) public object GPUAndroidHardwareBufferUtil { public fun createTexture(
diff --git a/tools/android/webgpu/src/test/java/androidx/webgpu/MappedNamedConstantsTest.kt b/tools/android/webgpu/src/test/java/androidx/webgpu/MappedNamedConstantsTest.kt index 6ff46b3..e825d51 100644 --- a/tools/android/webgpu/src/test/java/androidx/webgpu/MappedNamedConstantsTest.kt +++ b/tools/android/webgpu/src/test/java/androidx/webgpu/MappedNamedConstantsTest.kt
@@ -34,6 +34,7 @@ } } +@OptIn(ExperimentalWebGpuApi::class) class MappedNamedConstantsTest { val TYPES_WITH_MAPPED_NAMED_CONSTANTS = arrayOf(