summaryrefslogtreecommitdiffstats
path: root/dom/webgpu
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/webgpu
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webgpu')
-rw-r--r--dom/webgpu/Adapter.cpp231
-rw-r--r--dom/webgpu/Adapter.h107
-rw-r--r--dom/webgpu/BindGroup.cpp36
-rw-r--r--dom/webgpu/BindGroup.h33
-rw-r--r--dom/webgpu/BindGroupLayout.cpp36
-rw-r--r--dom/webgpu/BindGroupLayout.h34
-rw-r--r--dom/webgpu/Buffer.cpp348
-rw-r--r--dom/webgpu/Buffer.h95
-rw-r--r--dom/webgpu/CanvasContext.cpp310
-rw-r--r--dom/webgpu/CanvasContext.h108
-rw-r--r--dom/webgpu/CommandBuffer.cpp51
-rw-r--r--dom/webgpu/CommandBuffer.h40
-rw-r--r--dom/webgpu/CommandEncoder.cpp235
-rw-r--r--dom/webgpu/CommandEncoder.h107
-rw-r--r--dom/webgpu/CompilationInfo.cpp34
-rw-r--r--dom/webgpu/CompilationInfo.h39
-rw-r--r--dom/webgpu/CompilationMessage.cpp24
-rw-r--r--dom/webgpu/CompilationMessage.h54
-rw-r--r--dom/webgpu/ComputePassEncoder.cpp108
-rw-r--r--dom/webgpu/ComputePassEncoder.h75
-rw-r--r--dom/webgpu/ComputePipeline.cpp50
-rw-r--r--dom/webgpu/ComputePipeline.h41
-rw-r--r--dom/webgpu/Device.cpp371
-rw-r--r--dom/webgpu/Device.h171
-rw-r--r--dom/webgpu/DeviceLostInfo.cpp13
-rw-r--r--dom/webgpu/DeviceLostInfo.h51
-rw-r--r--dom/webgpu/Instance.cpp112
-rw-r--r--dom/webgpu/Instance.h60
-rw-r--r--dom/webgpu/ObjectModel.cpp40
-rw-r--r--dom/webgpu/ObjectModel.h131
-rw-r--r--dom/webgpu/OutOfMemoryError.cpp17
-rw-r--r--dom/webgpu/OutOfMemoryError.h35
-rw-r--r--dom/webgpu/PipelineLayout.cpp36
-rw-r--r--dom/webgpu/PipelineLayout.h33
-rw-r--r--dom/webgpu/QuerySet.cpp22
-rw-r--r--dom/webgpu/QuerySet.h31
-rw-r--r--dom/webgpu/Queue.cpp475
-rw-r--r--dom/webgpu/Queue.h74
-rw-r--r--dom/webgpu/RenderBundle.cpp38
-rw-r--r--dom/webgpu/RenderBundle.h32
-rw-r--r--dom/webgpu/RenderBundleEncoder.cpp196
-rw-r--r--dom/webgpu/RenderBundleEncoder.h77
-rw-r--r--dom/webgpu/RenderPassEncoder.cpp330
-rw-r--r--dom/webgpu/RenderPassEncoder.h104
-rw-r--r--dom/webgpu/RenderPipeline.cpp50
-rw-r--r--dom/webgpu/RenderPipeline.h41
-rw-r--r--dom/webgpu/Sampler.cpp32
-rw-r--r--dom/webgpu/Sampler.h33
-rw-r--r--dom/webgpu/ShaderModule.cpp40
-rw-r--r--dom/webgpu/ShaderModule.h40
-rw-r--r--dom/webgpu/SupportedFeatures.cpp18
-rw-r--r--dom/webgpu/SupportedFeatures.h29
-rw-r--r--dom/webgpu/SupportedLimits.cpp101
-rw-r--r--dom/webgpu/SupportedLimits.h61
-rw-r--r--dom/webgpu/Texture.cpp132
-rw-r--r--dom/webgpu/Texture.h73
-rw-r--r--dom/webgpu/TextureView.cpp37
-rw-r--r--dom/webgpu/TextureView.h35
-rw-r--r--dom/webgpu/Utility.cpp53
-rw-r--r--dom/webgpu/Utility.h40
-rw-r--r--dom/webgpu/ValidationError.cpp41
-rw-r--r--dom/webgpu/ValidationError.h47
-rw-r--r--dom/webgpu/ipc/PWebGPU.ipdl93
-rw-r--r--dom/webgpu/ipc/PWebGPUTypes.ipdlh26
-rw-r--r--dom/webgpu/ipc/WebGPUChild.cpp1249
-rw-r--r--dom/webgpu/ipc/WebGPUChild.h146
-rw-r--r--dom/webgpu/ipc/WebGPUParent.cpp1128
-rw-r--r--dom/webgpu/ipc/WebGPUParent.h156
-rw-r--r--dom/webgpu/ipc/WebGPUSerialize.h50
-rw-r--r--dom/webgpu/ipc/WebGPUTypes.h69
-rw-r--r--dom/webgpu/mochitest/mochitest-no-pref.ini10
-rw-r--r--dom/webgpu/mochitest/mochitest.ini42
-rw-r--r--dom/webgpu/mochitest/test_basic_canvas.worker.html18
-rw-r--r--dom/webgpu/mochitest/test_basic_canvas.worker.js32
-rw-r--r--dom/webgpu/mochitest/test_buffer_mapping.html73
-rw-r--r--dom/webgpu/mochitest/test_command_buffer_creation.html29
-rw-r--r--dom/webgpu/mochitest/test_device_creation.html29
-rw-r--r--dom/webgpu/mochitest/test_disabled.html17
-rw-r--r--dom/webgpu/mochitest/test_enabled.html17
-rw-r--r--dom/webgpu/mochitest/test_error_scope.html39
-rw-r--r--dom/webgpu/mochitest/test_insecure_context.html22
-rw-r--r--dom/webgpu/mochitest/test_queue_copyExternalImageToTexture.html261
-rw-r--r--dom/webgpu/mochitest/test_queue_write.html50
-rw-r--r--dom/webgpu/mochitest/test_submit_compute_empty.html32
-rw-r--r--dom/webgpu/mochitest/test_submit_render_empty.html57
-rw-r--r--dom/webgpu/mochitest/test_submit_render_empty.worker.html14
-rw-r--r--dom/webgpu/mochitest/test_submit_render_empty.worker.js48
-rw-r--r--dom/webgpu/mochitest/worker_wrapper.js33
-rw-r--r--dom/webgpu/moz.build76
-rw-r--r--dom/webgpu/tests/cts/README.md17
-rw-r--r--dom/webgpu/tests/cts/arguments.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/.eslint-resolver.js23
-rw-r--r--dom/webgpu/tests/cts/checkout/.eslintignore1
-rw-r--r--dom/webgpu/tests/cts/checkout/.eslintrc.json127
-rw-r--r--dom/webgpu/tests/cts/checkout/.github/pull_request_template.md21
-rw-r--r--dom/webgpu/tests/cts/checkout/.github/workflows/pr.yml28
-rw-r--r--dom/webgpu/tests/cts/checkout/.github/workflows/push.yml26
-rw-r--r--dom/webgpu/tests/cts/checkout/.github/workflows/workflow.yml80
-rw-r--r--dom/webgpu/tests/cts/checkout/.gitignore196
-rw-r--r--dom/webgpu/tests/cts/checkout/CONTRIBUTING.md31
-rw-r--r--dom/webgpu/tests/cts/checkout/Gruntfile.js229
-rw-r--r--dom/webgpu/tests/cts/checkout/LICENSE.txt26
-rw-r--r--dom/webgpu/tests/cts/checkout/README.md22
-rw-r--r--dom/webgpu/tests/cts/checkout/babel.config.js21
-rw-r--r--dom/webgpu/tests/cts/checkout/cts.code-workspace110
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/build.md43
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/deno.md24
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/fp_primer.md516
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/helper_index.txt92
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/implementing.md97
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/README.md99
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/convert_to_issue.pngbin0 -> 2061 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/developing.md134
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/life_of.md46
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/plans.md82
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/intro/tests.md25
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/organization.md166
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/reviews.md70
-rw-r--r--dom/webgpu/tests/cts/checkout/docs/terms.md270
-rw-r--r--dom/webgpu/tests/cts/checkout/node.tsconfig.json20
-rw-r--r--dom/webgpu/tests/cts/checkout/package-lock.json15798
-rw-r--r--dom/webgpu/tests/cts/checkout/package.json77
-rw-r--r--dom/webgpu/tests/cts/checkout/prettier.config.js8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts120
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts328
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts337
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts110
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts95
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts44
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts30
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts158
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/params_utils.ts124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts94
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/encode_selectively.ts23
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/json_param_value.ts83
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts155
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts262
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/separators.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/stringify_params.ts44
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/validQueryPart.ts2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/stack.ts82
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts646
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts15
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts575
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/util.ts10
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/version.ts1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts278
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/sys.ts46
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts32
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts44
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts227
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts625
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts83
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/templates/cts.https.html32
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/.eslintrc.json9
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/checklist.ts138
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts102
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts189
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts144
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts64
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts122
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/image_utils.ts58
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/presubmit.ts19
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/run_wpt_ref_tests.ts446
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/setup-ts-in-node.js51
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/version.ts4
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts58
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/colors.ts127
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts74
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts149
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts7
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/types.ts59
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/util.ts303
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a.spec.ts8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a/b.spec.ts6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a/b/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a/b/c.spec.ts80
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/a/b/d.spec.ts8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/file_depth_2/in_single_child_dir/r.spec.ts6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/json.spec.ts10
-rw-r--r--dom/webgpu/tests/cts/checkout/src/demo/subcases.spec.ts38
-rw-r--r--dom/webgpu/tests/cts/checkout/src/external/README.md31
-rw-r--r--dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts471
-rw-r--r--dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js1228
-rw-r--r--dom/webgpu/tests/cts/checkout/src/manual/README.txt18
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/README.md2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.bt2020.vp9.webmbin0 -> 4057 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.bt601.vp9.webmbin0 -> 4015 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.bt709.vp9.webmbin0 -> 4075 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.mp4bin0 -> 92225 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.theora.ogvbin0 -> 10292 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/red-green.webmvp8.webmbin0 -> 10979 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/resources/webgpu.pngbin0 -> 33475 bytes
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/README.txt6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/adapter/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/adapter/device_allocation.spec.ts290
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/compute/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/compute/compute_pass.spec.ts243
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/README.txt2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_allocation.spec.ts65
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_layout_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/buffer_allocation.spec.ts25
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/command_encoder_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/compute_pipeline_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/pipeline_layout_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/query_set_allocation.spec.ts27
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/render_bundle_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/render_pipeline_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/sampler_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/shader_module_allocation.spec.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/device/texture_allocation.spec.ts27
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/listing.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/memory/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/memory/churn.spec.ts17
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts45
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queries/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queries/occlusion.spec.ts10
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queries/pipeline_statistics.spec.ts38
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queries/resolve.spec.ts15
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queries/timestamps.spec.ts50
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queue/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/queue/submit.spec.ts102
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/render/README.txt3
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/render/render_pass.spec.ts353
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/render/vertex_buffers.spec.ts130
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/shaders/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/shaders/entry_points.spec.ts78
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/shaders/non_halting.spec.ts194
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/shaders/slow.spec.ts195
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/texture/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/texture/large.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts167
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts35
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts408
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts3418
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts138
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/listing.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts931
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts147
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts1021
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts440
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts112
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts207
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts133
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts268
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts259
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts351
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts143
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts3
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/README.txt2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapter.spec.ts124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapterInfo.spec.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts277
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/async_ordering/README.txt12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts499
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_ArrayBuffer.spec.ts89
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_detach.spec.ts79
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_oom.spec.ts120
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/mapping_test.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/threading.spec.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/basic.spec.ts98
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/clearBuffer.spec.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyBufferToBuffer.spec.ts108
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts1597
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts1983
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/programmable_state_test.ts157
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/state_tracking.spec.ts306
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/README.txt8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/dynamic_state.spec.ts19
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/state_tracking.spec.ts631
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute/basic.spec.ts163
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/entry_point_name.spec.ts12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/overrides.spec.ts503
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/device/lost.spec.ts92
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/labels.spec.ts12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_allocation/README.txt7
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/buffer_sync_test.ts938
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/multiple_buffers.spec.ts354
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/single_buffer.spec.ts257
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/operation_context_helper.ts334
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts709
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/texture_sync_test.ts124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/onSubmittedWorkDone.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/pipeline/default_layout.spec.ts27
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/queue/writeBuffer.spec.ts235
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts137
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/clear_value.spec.ts189
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/resolve.spec.ts205
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeOp.spec.ts354
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeop2.spec.ts83
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/alpha_to_coverage.spec.ts19
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/culling_tests.spec.ts185
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/overrides.spec.ts456
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/pipeline_output_targets.spec.ts458
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/primitive_topology.spec.ts498
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts519
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/vertex_only_render_pipeline.spec.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/basic.spec.ts353
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts890
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts549
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts369
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts524
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/draw.spec.ts750
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/indirect_draw.spec.ts251
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/robust_access_index.spec.ts8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts583
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/buffer.spec.ts899
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts66
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts197
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts157
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts645
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts320
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/filter_mode.spec.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/lod_clamp.spec.ts12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts197
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts362
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/read.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/threading/README.txt11
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/uncapturederror.spec.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/correctness.spec.ts1095
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/index_format.spec.ts584
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/regression/README.txt2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/create.spec.ts121
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/destroy.spec.ts101
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts1124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/threading.spec.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/README.txt10
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts76
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts445
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/README.txt8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts669
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts1131
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts456
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createPipelineLayout.spec.ts156
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createSampler.spec.ts59
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts879
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts332
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/debugMarker.spec.ts98
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginComputePass.spec.ts193
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginRenderPass.spec.ts211
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/clearBuffer.spec.ts246
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/compute_pass.spec.ts250
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyBufferToBuffer.spec.ts326
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts876
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/debug.spec.ts64
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/index_access.spec.ts162
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts862
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts319
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts202
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts124
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts62
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts141
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts184
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render_pass.spec.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts446
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts240
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts587
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_state.spec.ts204
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts790
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/begin_end.spec.ts162
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/common.ts37
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts157
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/pipeline_statistics.spec.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/resolveQuerySet.spec.ts181
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts258
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts283
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/getBindGroupLayout.spec.ts201
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/README.txt32
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_related.spec.ts229
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts454
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts267
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/layout_related.spec.ts479
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts538
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts14
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/create.spec.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/destroy.spec.ts15
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/README.txt13
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/buffer_mapped.spec.ts280
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts904
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts63
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/submit.spec.ts47
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeBuffer.spec.ts200
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeTexture.spec.ts110
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts639
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts1129
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts192
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/storeOp.spec.ts75
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts203
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts392
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts324
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts94
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/multisample_state.spec.ts83
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/overrides.spec.ts501
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/primitive_state.spec.ts42
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/shader_module.spec.ts112
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/vertex_state.spec.ts649
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_encoder.spec.ts910
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_misc.spec.ts409
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts1376
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts572
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts420
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts117
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/overrides.spec.ts96
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/README.txt5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts962
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/destroy.spec.ts119
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/rg11b10ufloat_renderable.spec.ts108
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts448
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts1123
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts62
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/examples.spec.ts274
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts1067
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/README.txt7
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/constants/flags.spec.ts79
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.html.ts52
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.http.html11
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.https.html11
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/idl/idl_test.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/listing.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/evaluation_order.spec.ts484
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts9
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts220
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts143
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_arithmetic.spec.ts194
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts260
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts156
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts213
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts167
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts65
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts92
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts92
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/arrayLength.spec.ts16
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts76
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAdd.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAnd.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicCompareExchangeWeak.spec.ts49
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicExchange.spec.ts33
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicLoad.spec.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMax.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMin.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicOr.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicStore.spec.ts33
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicSub.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicXor.spec.ts39
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts72
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts172
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts250
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts249
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts250
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts66
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts32
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts172
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts156
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts23
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts337
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts201
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts347
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts250
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts73
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts80
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidth.spec.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthCoarse.spec.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthFine.spec.ts21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts386
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts63
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts95
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts107
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts123
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts122
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts93
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts356
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts89
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts88
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts55
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts55
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts60
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts60
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts66
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts70
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts137
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts196
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts250
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts229
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts53
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts67
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts70
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts85
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/storageBarrier.spec.ts38
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts53
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts160
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGather.spec.ts270
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGatherCompare.spec.ts134
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts185
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts100
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLevels.spec.ts65
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumSamples.spec.ts37
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts273
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts163
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts145
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompareLevel.spec.ts149
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleGrad.spec.ts136
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleLevel.spec.ts274
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts122
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts46
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts40
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/utils.ts45
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupBarrier.spec.ts38
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts209
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts1137
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts33
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts41
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts37
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/atomicity.spec.ts102
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts211
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/coherence.spec.ts525
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts1049
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/weak.spec.ts429
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/padding.spec.ts423
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts480
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts610
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts297
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/shared_structs.spec.ts353
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts448
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/regression/README.txt2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts209
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/align.spec.ts180
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/blankspace.spec.ts50
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/builtin.spec.ts37
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/comments.spec.ts75
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/identifiers.spec.ts277
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/literal.spec.ts296
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts269
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/source.spec.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/static_assert.spec.ts37
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/var_and_let.spec.ts72
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/bindings.spec.ts118
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/util.ts91
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts277
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/entry_point.spec.ts141
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/interpolate.spec.ts144
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/invariant.spec.ts88
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts259
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/util.ts79
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_validation_test.ts76
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/static_assert/static_assert.spec.ts70
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/shader/values.ts91
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/buffer.ts23
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts245
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts261
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/command_buffer_maker.ts85
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts282
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts587
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts1076
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/copy_to_texture.ts194
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/create_elements.ts95
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts391
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/f32_interval.ts2136
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts962
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/memory.ts25
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts51
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts196
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts213
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/data_generation.ts83
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/layout.ts370
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/subresource.ts68
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts349
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts918
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts160
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.spec.ts159
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts341
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/unions.ts45
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/README.txt5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts424
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/context_creation.spec.ts47
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts262
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getPreferredCanvasFormat.spec.ts19
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts473
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/ImageBitmap.spec.ts581
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts764
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts15
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts439
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/README.txt17
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.html.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.https.html12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace.html.ts82
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html23
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex.html.ts771
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_copy.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_draw.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_copy.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_draw.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_store.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_copy.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_draw.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_store.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha.html.ts177
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_copy.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_draw.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_copy.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_draw.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_copy.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_draw.https.html21
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.html.ts79
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html15
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/create-pattern-data-url.ts23
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/gpu_ref_test.ts26
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_clear-ref.html22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html17
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html.ts36
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_complex-ref.html26
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_opaque-ref.html26
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_premultiplied-ref.html26
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html25
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/resize_observer-ref.html90
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.html.ts150
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.https.html24
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts185
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts35
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts79
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts16
-rw-r--r--dom/webgpu/tests/cts/checkout/standalone/index.html423
-rw-r--r--dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/LICENSE.txt9
-rw-r--r--dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/jquery-3.3.1.min.js2
-rw-r--r--dom/webgpu/tests/cts/checkout/standalone/third_party/normalize.min.css1
-rw-r--r--dom/webgpu/tests/cts/checkout/standalone/webgpu-logo-notext.svg34
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/checklist11
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/dev_server4
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/gen_cache4
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/gen_listings7
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/gen_version33
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/gen_wpt_cts_html39
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/presubmit4
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/run_deno3
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/run_node6
-rw-r--r--dom/webgpu/tests/cts/checkout/tools/run_wpt_ref_tests4
-rw-r--r--dom/webgpu/tests/cts/checkout/tsconfig.json49
-rw-r--r--dom/webgpu/tests/cts/checkout/w3c.json5
-rw-r--r--dom/webgpu/tests/cts/checkout_commit.txt1
-rw-r--r--dom/webgpu/tests/cts/myexpectations.txt0
-rw-r--r--dom/webgpu/tests/cts/vendor/Cargo.lock889
-rw-r--r--dom/webgpu/tests/cts/vendor/Cargo.toml20
-rw-r--r--dom/webgpu/tests/cts/vendor/src/fs.rs310
-rw-r--r--dom/webgpu/tests/cts/vendor/src/main.rs458
-rw-r--r--dom/webgpu/tests/cts/vendor/src/path.rs23
-rw-r--r--dom/webgpu/tests/cts/vendor/src/process.rs85
714 files changed, 138550 insertions, 0 deletions
diff --git a/dom/webgpu/Adapter.cpp b/dom/webgpu/Adapter.cpp
new file mode 100644
index 0000000000..381378206b
--- /dev/null
+++ b/dom/webgpu/Adapter.cpp
@@ -0,0 +1,231 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "Adapter.h"
+
+#include "Device.h"
+#include "Instance.h"
+#include "SupportedFeatures.h"
+#include "SupportedLimits.h"
+#include "ipc/WebGPUChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla::webgpu {
+
+bool AdapterInfo::WrapObject(JSContext* const cx,
+ JS::Handle<JSObject*> givenProto,
+ JS::MutableHandle<JSObject*> reflector) {
+ return dom::GPUAdapterInfo_Binding::Wrap(cx, this, givenProto, reflector);
+}
+
+void AdapterInfo::GetWgpuName(nsString& s) const {
+ s = mAboutSupportInfo->name;
+}
+
+uint32_t AdapterInfo::WgpuVendor() const { return mAboutSupportInfo->vendor; }
+
+uint32_t AdapterInfo::WgpuDevice() const { return mAboutSupportInfo->device; }
+
+void AdapterInfo::GetWgpuDeviceType(nsString& s) const {
+ switch (mAboutSupportInfo->device_type) {
+ case ffi::WGPUDeviceType_Cpu:
+ s.AssignLiteral("Cpu");
+ return;
+ case ffi::WGPUDeviceType_DiscreteGpu:
+ s.AssignLiteral("DiscreteGpu");
+ return;
+ case ffi::WGPUDeviceType_IntegratedGpu:
+ s.AssignLiteral("IntegratedGpu");
+ return;
+ case ffi::WGPUDeviceType_VirtualGpu:
+ s.AssignLiteral("VirtualGpu");
+ return;
+ case ffi::WGPUDeviceType_Other:
+ s.AssignLiteral("Other");
+ return;
+ case ffi::WGPUDeviceType_Sentinel:
+ break;
+ }
+ MOZ_CRASH("Bad `ffi::WGPUDeviceType`");
+}
+
+void AdapterInfo::GetWgpuDriver(nsString& s) const {
+ s = mAboutSupportInfo->driver;
+}
+
+void AdapterInfo::GetWgpuDriverInfo(nsString& s) const {
+ s = mAboutSupportInfo->driver_info;
+}
+
+void AdapterInfo::GetWgpuBackend(nsString& s) const {
+ switch (mAboutSupportInfo->backend) {
+ case ffi::WGPUBackend_Empty:
+ s.AssignLiteral("Empty");
+ return;
+ case ffi::WGPUBackend_Vulkan:
+ s.AssignLiteral("Vulkan");
+ return;
+ case ffi::WGPUBackend_Metal:
+ s.AssignLiteral("Metal");
+ return;
+ case ffi::WGPUBackend_Dx12:
+ s.AssignLiteral("Dx12");
+ return;
+ case ffi::WGPUBackend_Dx11:
+ s.AssignLiteral("Dx11");
+ return;
+ case ffi::WGPUBackend_Gl:
+ s.AssignLiteral("Gl");
+ return;
+ case ffi::WGPUBackend_BrowserWebGpu: // This should never happen, because
+ // we _are_ the browser.
+ case ffi::WGPUBackend_Sentinel:
+ break;
+ }
+ MOZ_CRASH("Bad `ffi::WGPUBackend`");
+}
+
+// -
+
+GPU_IMPL_CYCLE_COLLECTION(Adapter, mParent, mBridge, mFeatures, mLimits)
+GPU_IMPL_JS_WRAP(Adapter)
+
+Maybe<uint32_t> Adapter::MakeFeatureBits(
+ const dom::Sequence<dom::GPUFeatureName>& aFeatures) {
+ uint32_t bits = 0;
+ for (const auto& feature : aFeatures) {
+ if (feature == dom::GPUFeatureName::Depth_clip_control) {
+ bits |= WGPUFeatures_DEPTH_CLIP_CONTROL;
+ } else if (feature == dom::GPUFeatureName::Texture_compression_bc) {
+ bits |= WGPUFeatures_TEXTURE_COMPRESSION_BC;
+ } else if (feature == dom::GPUFeatureName::Indirect_first_instance) {
+ bits |= WGPUFeatures_INDIRECT_FIRST_INSTANCE;
+ } else if (feature == dom::GPUFeatureName::Depth32float_stencil8) {
+ bits |= WGPUFeatures_DEPTH32FLOAT_STENCIL8;
+ } else {
+ NS_WARNING(
+ nsPrintfCString("Requested feature bit '%d' is not recognized.",
+ static_cast<int>(feature))
+ .get());
+ return Nothing();
+ }
+ }
+ return Some(bits);
+}
+
+Adapter::Adapter(Instance* const aParent, WebGPUChild* const aBridge,
+ const std::shared_ptr<ffi::WGPUAdapterInformation>& aInfo)
+ : ChildOf(aParent),
+ mBridge(aBridge),
+ mId(aInfo->id),
+ mFeatures(new SupportedFeatures(this)),
+ mLimits(new SupportedLimits(this,
+ MakeUnique<ffi::WGPULimits>(aInfo->limits))),
+ mInfo(aInfo) {
+ ErrorResult result; // TODO: should this come from outside
+ // This list needs to match `AdapterRequestDevice`
+ if (aInfo->features & WGPUFeatures_DEPTH_CLIP_CONTROL) {
+ dom::GPUSupportedFeatures_Binding::SetlikeHelpers::Add(
+ mFeatures, u"depth-clip-control"_ns, result);
+ }
+ if (aInfo->features & WGPUFeatures_TEXTURE_COMPRESSION_BC) {
+ dom::GPUSupportedFeatures_Binding::SetlikeHelpers::Add(
+ mFeatures, u"texture-compression-bc"_ns, result);
+ }
+ if (aInfo->features & WGPUFeatures_INDIRECT_FIRST_INSTANCE) {
+ dom::GPUSupportedFeatures_Binding::SetlikeHelpers::Add(
+ mFeatures, u"indirect-first-instance"_ns, result);
+ }
+ if (aInfo->features & WGPUFeatures_DEPTH32FLOAT_STENCIL8) {
+ dom::GPUSupportedFeatures_Binding::SetlikeHelpers::Add(
+ mFeatures, u"depth32float-stencil8"_ns, result);
+ }
+}
+
+Adapter::~Adapter() { Cleanup(); }
+
+void Adapter::Cleanup() {
+ if (mValid && mBridge && mBridge->CanSend()) {
+ mValid = false;
+ mBridge->SendAdapterDestroy(mId);
+ }
+}
+
+const RefPtr<SupportedFeatures>& Adapter::Features() const { return mFeatures; }
+const RefPtr<SupportedLimits>& Adapter::Limits() const { return mLimits; }
+bool Adapter::IsFallbackAdapter() const {
+ return mInfo->device_type == ffi::WGPUDeviceType::WGPUDeviceType_Cpu;
+}
+
+already_AddRefed<dom::Promise> Adapter::RequestDevice(
+ const dom::GPUDeviceDescriptor& aDesc, ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (!mBridge->CanSend()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "WebGPUChild cannot send, must recreate Adapter");
+ return promise.forget();
+ }
+
+ ffi::WGPULimits limits = {};
+ auto request = mBridge->AdapterRequestDevice(mId, aDesc, &limits);
+ if (request) {
+ RefPtr<Device> device =
+ new Device(this, request->mId, MakeUnique<ffi::WGPULimits>(limits));
+ // copy over the features
+ for (const auto& feature : aDesc.mRequiredFeatures) {
+ NS_ConvertASCIItoUTF16 string(
+ dom::GPUFeatureNameValues::GetString(feature));
+ dom::GPUSupportedFeatures_Binding::SetlikeHelpers::Add(device->mFeatures,
+ string, aRv);
+ }
+
+ request->mPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, device](bool aSuccess) {
+ if (aSuccess) {
+ promise->MaybeResolve(device);
+ } else {
+ // In this path, request->mId has an error entry in the wgpu
+ // registry, so let Device::~Device clean things up on both the
+ // child and parent side.
+ promise->MaybeRejectWithInvalidStateError(
+ "Unable to fulfill requested features and limits");
+ }
+ },
+ [promise, device](const ipc::ResponseRejectReason& aReason) {
+ // We can't be sure how far along the WebGPUParent got in handling
+ // our AdapterRequestDevice message, but we can't communicate with it,
+ // so clear up our client state for this Device without trying to
+ // communicate with the parent about it.
+ device->CleanupUnregisteredInParent();
+ promise->MaybeRejectWithNotSupportedError("IPC error");
+ });
+ } else {
+ promise->MaybeRejectWithNotSupportedError("Unable to instantiate a Device");
+ }
+
+ return promise.forget();
+}
+
+// -
+
+already_AddRefed<dom::Promise> Adapter::RequestAdapterInfo(
+ const dom::Sequence<nsString>& /*aUnmaskHints*/, ErrorResult& aRv) const {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (!promise) return nullptr;
+
+ auto rai = UniquePtr<AdapterInfo>{new AdapterInfo(mInfo)};
+ promise->MaybeResolve(std::move(rai));
+ return promise.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Adapter.h b/dom/webgpu/Adapter.h
new file mode 100644
index 0000000000..cd46bc3ddf
--- /dev/null
+++ b/dom/webgpu/Adapter.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_Adapter_H_
+#define GPU_Adapter_H_
+
+#include <memory>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/NonRefcountedDOMObject.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsString.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+class Promise;
+struct GPUDeviceDescriptor;
+struct GPUExtensions;
+struct GPUFeatures;
+enum class GPUFeatureName : uint8_t;
+enum class WgpuBackend : uint8_t;
+enum class WgpuDeviceType : uint8_t;
+template <typename T>
+class Sequence;
+} // namespace dom
+
+namespace webgpu {
+class Device;
+class Instance;
+class SupportedFeatures;
+class SupportedLimits;
+class WebGPUChild;
+namespace ffi {
+struct WGPUAdapterInformation;
+} // namespace ffi
+
+class AdapterInfo final : public dom::NonRefcountedDOMObject {
+ private:
+ const std::shared_ptr<ffi::WGPUAdapterInformation> mAboutSupportInfo;
+
+ public:
+ explicit AdapterInfo(
+ const std::shared_ptr<ffi::WGPUAdapterInformation>& aAboutSupportInfo)
+ : mAboutSupportInfo(aAboutSupportInfo) {}
+
+ void GetVendor(nsString& s) const { s = nsString(); }
+ void GetArchitecture(nsString& s) const { s = nsString(); }
+ void GetDevice(nsString& s) const { s = nsString(); }
+ void GetDescription(nsString& s) const { s = nsString(); }
+
+ // Non-standard field getters; see also TODO BUGZILLA LINK
+ void GetWgpuName(nsString&) const;
+ uint32_t WgpuVendor() const;
+ uint32_t WgpuDevice() const;
+ void GetWgpuDeviceType(nsString&) const;
+ void GetWgpuDriver(nsString&) const;
+ void GetWgpuDriverInfo(nsString&) const;
+ void GetWgpuBackend(nsString&) const;
+
+ bool WrapObject(JSContext*, JS::Handle<JSObject*>,
+ JS::MutableHandle<JSObject*>);
+};
+
+class Adapter final : public ObjectBase, public ChildOf<Instance> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(Adapter)
+ GPU_DECL_JS_WRAP(Adapter)
+
+ RefPtr<WebGPUChild> mBridge;
+
+ static Maybe<uint32_t> MakeFeatureBits(
+ const dom::Sequence<dom::GPUFeatureName>& aFeatures);
+
+ private:
+ ~Adapter();
+ void Cleanup();
+
+ const RawId mId;
+ // Cant have them as `const` right now, since we wouldn't be able
+ // to unlink them in CC unlink.
+ RefPtr<SupportedFeatures> mFeatures;
+ RefPtr<SupportedLimits> mLimits;
+
+ const std::shared_ptr<ffi::WGPUAdapterInformation> mInfo;
+
+ public:
+ Adapter(Instance* const aParent, WebGPUChild* const aBridge,
+ const std::shared_ptr<ffi::WGPUAdapterInformation>& aInfo);
+ const RefPtr<SupportedFeatures>& Features() const;
+ const RefPtr<SupportedLimits>& Limits() const;
+ bool IsFallbackAdapter() const;
+
+ already_AddRefed<dom::Promise> RequestDevice(
+ const dom::GPUDeviceDescriptor& aDesc, ErrorResult& aRv);
+
+ already_AddRefed<dom::Promise> RequestAdapterInfo(
+ const dom::Sequence<nsString>& aUnmaskHints, ErrorResult& aRv) const;
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_Adapter_H_
diff --git a/dom/webgpu/BindGroup.cpp b/dom/webgpu/BindGroup.cpp
new file mode 100644
index 0000000000..d03aaefee9
--- /dev/null
+++ b/dom/webgpu/BindGroup.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "BindGroup.h"
+#include "ipc/WebGPUChild.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(BindGroup, mParent)
+GPU_IMPL_JS_WRAP(BindGroup)
+
+BindGroup::BindGroup(Device* const aParent, RawId aId)
+ : ChildOf(aParent), mId(aId) {
+ if (!aId) {
+ mValid = false;
+ }
+}
+
+BindGroup::~BindGroup() { Cleanup(); }
+
+void BindGroup::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendBindGroupDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/BindGroup.h b/dom/webgpu/BindGroup.h
new file mode 100644
index 0000000000..4f67c906f3
--- /dev/null
+++ b/dom/webgpu/BindGroup.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_BindGroup_H_
+#define GPU_BindGroup_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class BindGroup final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(BindGroup)
+ GPU_DECL_JS_WRAP(BindGroup)
+
+ BindGroup(Device* const aParent, RawId aId);
+
+ const RawId mId;
+
+ private:
+ ~BindGroup();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_BindGroup_H_
diff --git a/dom/webgpu/BindGroupLayout.cpp b/dom/webgpu/BindGroupLayout.cpp
new file mode 100644
index 0000000000..27ecbd3684
--- /dev/null
+++ b/dom/webgpu/BindGroupLayout.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "BindGroupLayout.h"
+#include "ipc/WebGPUChild.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(BindGroupLayout, mParent)
+GPU_IMPL_JS_WRAP(BindGroupLayout)
+
+BindGroupLayout::BindGroupLayout(Device* const aParent, RawId aId, bool aOwning)
+ : ChildOf(aParent), mId(aId), mOwning(aOwning) {
+ if (!aId) {
+ mValid = false;
+ }
+}
+
+BindGroupLayout::~BindGroupLayout() { Cleanup(); }
+
+void BindGroupLayout::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (mOwning && bridge && bridge->IsOpen()) {
+ bridge->SendBindGroupLayoutDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/BindGroupLayout.h b/dom/webgpu/BindGroupLayout.h
new file mode 100644
index 0000000000..fcd721ab5f
--- /dev/null
+++ b/dom/webgpu/BindGroupLayout.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_BindGroupLayout_H_
+#define GPU_BindGroupLayout_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class BindGroupLayout final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(BindGroupLayout)
+ GPU_DECL_JS_WRAP(BindGroupLayout)
+
+ BindGroupLayout(Device* const aParent, RawId aId, bool aOwning);
+
+ const RawId mId;
+ const bool mOwning;
+
+ private:
+ ~BindGroupLayout();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_BindGroupLayout_H_
diff --git a/dom/webgpu/Buffer.cpp b/dom/webgpu/Buffer.cpp
new file mode 100644
index 0000000000..9841a9b6d4
--- /dev/null
+++ b/dom/webgpu/Buffer.cpp
@@ -0,0 +1,348 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "Buffer.h"
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/ipc/Shmem.h"
+#include "ipc/WebGPUChild.h"
+#include "js/ArrayBuffer.h"
+#include "js/RootingAPI.h"
+#include "nsContentUtils.h"
+#include "nsWrapperCache.h"
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_JS_WRAP(Buffer)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer)
+ tmp->Drop();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Buffer)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Buffer)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+ if (tmp->mMapped) {
+ for (uint32_t i = 0; i < tmp->mMapped->mArrayBuffers.Length(); ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(
+ mMapped->mArrayBuffers[i])
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+Buffer::Buffer(Device* const aParent, RawId aId, BufferAddress aSize,
+ uint32_t aUsage, ipc::WritableSharedMemoryMapping&& aShmem)
+ : ChildOf(aParent), mId(aId), mSize(aSize), mUsage(aUsage) {
+ mozilla::HoldJSObjects(this);
+ mShmem =
+ std::make_shared<ipc::WritableSharedMemoryMapping>(std::move(aShmem));
+ MOZ_ASSERT(mParent);
+}
+
+Buffer::~Buffer() {
+ Drop();
+ mozilla::DropJSObjects(this);
+}
+
+already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId,
+ const dom::GPUBufferDescriptor& aDesc,
+ ErrorResult& aRv) {
+ if (aDevice->IsLost()) {
+ RefPtr<Buffer> buffer = new Buffer(aDevice, 0, aDesc.mSize, 0,
+ ipc::WritableSharedMemoryMapping());
+ return buffer.forget();
+ }
+
+ RefPtr<WebGPUChild> actor = aDevice->GetBridge();
+
+ auto handle = ipc::UnsafeSharedMemoryHandle();
+ auto mapping = ipc::WritableSharedMemoryMapping();
+
+ bool hasMapFlags = aDesc.mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
+ dom::GPUBufferUsage_Binding::MAP_READ);
+ if (hasMapFlags || aDesc.mMappedAtCreation) {
+ const auto checked = CheckedInt<size_t>(aDesc.mSize);
+ if (!checked.isValid()) {
+ aRv.ThrowRangeError("Mappable size is too large");
+ return nullptr;
+ }
+ size_t size = checked.value();
+
+ auto maybeShmem = ipc::UnsafeSharedMemoryHandle::CreateAndMap(size);
+
+ if (maybeShmem.isNothing()) {
+ aRv.ThrowAbortError(
+ nsPrintfCString("Unable to allocate shmem of size %" PRIuPTR, size));
+ return nullptr;
+ }
+
+ handle = std::move(maybeShmem.ref().first);
+ mapping = std::move(maybeShmem.ref().second);
+
+ MOZ_RELEASE_ASSERT(mapping.Size() >= size);
+
+ // zero out memory
+ memset(mapping.Bytes().data(), 0, size);
+ }
+
+ RawId id = actor->DeviceCreateBuffer(aDeviceId, aDesc, std::move(handle));
+
+ RefPtr<Buffer> buffer =
+ new Buffer(aDevice, id, aDesc.mSize, aDesc.mUsage, std::move(mapping));
+ if (aDesc.mMappedAtCreation) {
+ // Mapped at creation's raison d'être is write access, since the buffer is
+ // being created and there isn't anything interesting to read in it yet.
+ bool writable = true;
+ buffer->SetMapped(0, aDesc.mSize, writable);
+ }
+
+ return buffer.forget();
+}
+
+void Buffer::Drop() {
+ AbortMapRequest();
+
+ if (mMapped && !mMapped->mArrayBuffers.IsEmpty()) {
+ // The array buffers could live longer than us and our shmem, so make sure
+ // we clear the external buffer bindings.
+ dom::AutoJSAPI jsapi;
+ if (jsapi.Init(GetDevice().GetOwnerGlobal())) {
+ IgnoredErrorResult rv;
+ UnmapArrayBuffers(jsapi.cx(), rv);
+ }
+ }
+ mMapped.reset();
+
+ if (mValid && !GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferDrop(mId);
+ }
+ mValid = false;
+}
+
+void Buffer::SetMapped(BufferAddress aOffset, BufferAddress aSize,
+ bool aWritable) {
+ MOZ_ASSERT(!mMapped);
+ MOZ_RELEASE_ASSERT(aOffset <= mSize);
+ MOZ_RELEASE_ASSERT(aSize <= mSize - aOffset);
+
+ mMapped.emplace();
+ mMapped->mWritable = aWritable;
+ mMapped->mOffset = aOffset;
+ mMapped->mSize = aSize;
+}
+
+already_AddRefed<dom::Promise> Buffer::MapAsync(
+ uint32_t aMode, uint64_t aOffset, const dom::Optional<uint64_t>& aSize,
+ ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (GetDevice().IsLost()) {
+ promise->MaybeRejectWithOperationError("Device Lost");
+ return promise.forget();
+ }
+
+ if (mMapRequest) {
+ promise->MaybeRejectWithOperationError("Buffer mapping is already pending");
+ return promise.forget();
+ }
+
+ BufferAddress size = 0;
+ if (aSize.WasPassed()) {
+ size = aSize.Value();
+ } else if (aOffset <= mSize) {
+ // Default to passing the reminder of the buffer after the provided offset.
+ size = mSize - aOffset;
+ } else {
+ // The provided offset is larger than the buffer size.
+ // The parent side will handle the error, we can let the requested size be
+ // zero.
+ }
+
+ RefPtr<Buffer> self(this);
+
+ auto mappingPromise =
+ GetDevice().GetBridge()->SendBufferMap(mId, aMode, aOffset, size);
+ MOZ_ASSERT(mappingPromise);
+
+ mMapRequest = promise;
+
+ mappingPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, self](BufferMapResult&& aResult) {
+ // Unmap might have been called while the result was on the way back.
+ if (promise->State() != dom::Promise::PromiseState::Pending) {
+ return;
+ }
+
+ switch (aResult.type()) {
+ case BufferMapResult::TBufferMapSuccess: {
+ auto& success = aResult.get_BufferMapSuccess();
+ self->mMapRequest = nullptr;
+ self->SetMapped(success.offset(), success.size(),
+ success.writable());
+ promise->MaybeResolve(0);
+ break;
+ }
+ case BufferMapResult::TBufferMapError: {
+ auto& error = aResult.get_BufferMapError();
+ self->RejectMapRequest(promise, error.message());
+ break;
+ }
+ default: {
+ MOZ_CRASH("unreachable");
+ }
+ }
+ },
+ [promise](const ipc::ResponseRejectReason&) {
+ promise->MaybeRejectWithAbortError("Internal communication error!");
+ });
+
+ return promise.forget();
+}
+
+static void ExternalBufferFreeCallback(void* aContents, void* aUserData) {
+ Unused << aContents;
+ auto shm = static_cast<std::shared_ptr<ipc::WritableSharedMemoryMapping>*>(
+ aUserData);
+ delete shm;
+}
+
+void Buffer::GetMappedRange(JSContext* aCx, uint64_t aOffset,
+ const dom::Optional<uint64_t>& aSize,
+ JS::Rooted<JSObject*>* aObject, ErrorResult& aRv) {
+ if (!mMapped) {
+ aRv.ThrowInvalidStateError("Buffer is not mapped");
+ return;
+ }
+
+ const auto checkedOffset = CheckedInt<size_t>(aOffset);
+ const auto checkedSize = aSize.WasPassed()
+ ? CheckedInt<size_t>(aSize.Value())
+ : CheckedInt<size_t>(mSize) - aOffset;
+ const auto checkedMinBufferSize = checkedOffset + checkedSize;
+
+ if (!checkedOffset.isValid() || !checkedSize.isValid() ||
+ !checkedMinBufferSize.isValid() || aOffset < mMapped->mOffset ||
+ checkedMinBufferSize.value() > mMapped->mOffset + mMapped->mSize) {
+ aRv.ThrowRangeError("Invalid range");
+ return;
+ }
+
+ auto offset = checkedOffset.value();
+ auto size = checkedSize.value();
+ auto span = mShmem->Bytes().Subspan(offset, size);
+
+ std::shared_ptr<ipc::WritableSharedMemoryMapping>* userData =
+ new std::shared_ptr<ipc::WritableSharedMemoryMapping>(mShmem);
+ auto* const arrayBuffer = JS::NewExternalArrayBuffer(
+ aCx, size, span.data(), &ExternalBufferFreeCallback, userData);
+
+ if (!arrayBuffer) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ aObject->set(arrayBuffer);
+ mMapped->mArrayBuffers.AppendElement(*aObject);
+}
+
+void Buffer::UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv) {
+ MOZ_ASSERT(mMapped);
+
+ bool detachedArrayBuffers = true;
+ for (const auto& arrayBuffer : mMapped->mArrayBuffers) {
+ JS::Rooted<JSObject*> rooted(aCx, arrayBuffer);
+ if (!JS::DetachArrayBuffer(aCx, rooted)) {
+ detachedArrayBuffers = false;
+ }
+ };
+
+ mMapped->mArrayBuffers.Clear();
+
+ AbortMapRequest();
+
+ if (NS_WARN_IF(!detachedArrayBuffers)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+}
+
+void Buffer::RejectMapRequest(dom::Promise* aPromise, nsACString& message) {
+ if (mMapRequest == aPromise) {
+ mMapRequest = nullptr;
+ }
+
+ aPromise->MaybeRejectWithOperationError(message);
+}
+
+void Buffer::AbortMapRequest() {
+ if (mMapRequest) {
+ mMapRequest->MaybeRejectWithAbortError("Buffer unmapped");
+ }
+ mMapRequest = nullptr;
+}
+
+void Buffer::Unmap(JSContext* aCx, ErrorResult& aRv) {
+ if (!mMapped) {
+ return;
+ }
+
+ UnmapArrayBuffers(aCx, aRv);
+
+ bool hasMapFlags = mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
+ dom::GPUBufferUsage_Binding::MAP_READ);
+
+ if (!hasMapFlags) {
+ // We get here if the buffer was mapped at creation without map flags.
+ // It won't be possible to map the buffer again so we can get rid of
+ // our shmem on this side.
+ mShmem = std::make_shared<ipc::WritableSharedMemoryMapping>();
+ }
+
+ if (!GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferUnmap(GetDevice().mId, mId,
+ mMapped->mWritable);
+ }
+
+ mMapped.reset();
+}
+
+void Buffer::Destroy(JSContext* aCx, ErrorResult& aRv) {
+ if (mMapped) {
+ Unmap(aCx, aRv);
+ }
+
+ if (!GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferDestroy(mId);
+ }
+ // TODO: we don't have to implement it right now, but it's used by the
+ // examples
+}
+
+dom::GPUBufferMapState Buffer::MapState() const {
+ // Implementation reference:
+ // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
+
+ if (mMapped) {
+ return dom::GPUBufferMapState::Mapped;
+ }
+ if (mMapRequest) {
+ return dom::GPUBufferMapState::Pending;
+ }
+ return dom::GPUBufferMapState::Unmapped;
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Buffer.h b/dom/webgpu/Buffer.h
new file mode 100644
index 0000000000..2f809a4768
--- /dev/null
+++ b/dom/webgpu/Buffer.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_BUFFER_H_
+#define GPU_BUFFER_H_
+
+#include "js/RootingAPI.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsTArray.h"
+#include "ObjectModel.h"
+#include "mozilla/ipc/RawShmem.h"
+#include <memory>
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+struct GPUBufferDescriptor;
+template <typename T>
+class Optional;
+enum class GPUBufferMapState : uint8_t;
+} // namespace dom
+
+namespace webgpu {
+
+class Device;
+
+struct MappedInfo {
+ // True if mapping is requested for writing.
+ bool mWritable = false;
+ // Populated by `GetMappedRange`.
+ nsTArray<JS::Heap<JSObject*>> mArrayBuffers;
+ BufferAddress mOffset;
+ BufferAddress mSize;
+ MappedInfo() = default;
+ MappedInfo(const MappedInfo&) = delete;
+};
+
+class Buffer final : public ObjectBase, public ChildOf<Device> {
+ public:
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(Buffer)
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(Buffer)
+ GPU_DECL_JS_WRAP(Buffer)
+
+ static already_AddRefed<Buffer> Create(Device* aDevice, RawId aDeviceId,
+ const dom::GPUBufferDescriptor& aDesc,
+ ErrorResult& aRv);
+
+ already_AddRefed<dom::Promise> MapAsync(uint32_t aMode, uint64_t aOffset,
+ const dom::Optional<uint64_t>& aSize,
+ ErrorResult& aRv);
+ void GetMappedRange(JSContext* aCx, uint64_t aOffset,
+ const dom::Optional<uint64_t>& aSize,
+ JS::Rooted<JSObject*>* aObject, ErrorResult& aRv);
+ void Unmap(JSContext* aCx, ErrorResult& aRv);
+ void Destroy(JSContext* aCx, ErrorResult& aRv);
+
+ const RawId mId;
+
+ uint64_t Size() const { return mSize; }
+ uint32_t Usage() const { return mUsage; }
+ dom::GPUBufferMapState MapState() const;
+
+ private:
+ Buffer(Device* const aParent, RawId aId, BufferAddress aSize, uint32_t aUsage,
+ ipc::WritableSharedMemoryMapping&& aShmem);
+ virtual ~Buffer();
+ Device& GetDevice() { return *mParent; }
+ void Drop();
+ void UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv);
+ void RejectMapRequest(dom::Promise* aPromise, nsACString& message);
+ void AbortMapRequest();
+ void SetMapped(BufferAddress aOffset, BufferAddress aSize, bool aWritable);
+
+ // Note: we can't map a buffer with the size that don't fit into `size_t`
+ // (which may be smaller than `BufferAddress`), but general not all buffers
+ // are mapped.
+ const BufferAddress mSize;
+ const uint32_t mUsage;
+ nsString mLabel;
+ // Information about the currently active mapping.
+ Maybe<MappedInfo> mMapped;
+ RefPtr<dom::Promise> mMapRequest;
+ // mShmem does not point to a shared memory segment if the buffer is not
+ // mappable.
+ std::shared_ptr<ipc::WritableSharedMemoryMapping> mShmem;
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_BUFFER_H_
diff --git a/dom/webgpu/CanvasContext.cpp b/dom/webgpu/CanvasContext.cpp
new file mode 100644
index 0000000000..6d5b838bbd
--- /dev/null
+++ b/dom/webgpu/CanvasContext.cpp
@@ -0,0 +1,310 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "CanvasContext.h"
+#include "gfxUtils.h"
+#include "LayerUserData.h"
+#include "nsDisplayList.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/gfx/CanvasManagerChild.h"
+#include "mozilla/layers/CanvasRenderer.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/layers/WebRenderCanvasRenderer.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "ipc/WebGPUChild.h"
+
+namespace mozilla {
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback,
+ dom::GPUCanvasConfiguration& aField, const char* aName, uint32_t aFlags) {
+ aField.TraverseForCC(aCallback, aFlags);
+}
+
+inline void ImplCycleCollectionUnlink(dom::GPUCanvasConfiguration& aField) {
+ aField.UnlinkForCC();
+}
+
+// -
+
+template <class T>
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback,
+ const std::unique_ptr<T>& aField, const char* aName, uint32_t aFlags) {
+ if (aField) {
+ ImplCycleCollectionTraverse(aCallback, *aField, aName, aFlags);
+ }
+}
+
+template <class T>
+inline void ImplCycleCollectionUnlink(std::unique_ptr<T>& aField) {
+ aField = nullptr;
+}
+
+} // namespace mozilla
+
+// -
+
+namespace mozilla::webgpu {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(CanvasContext)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(CanvasContext)
+
+GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK_PTR(CanvasContext, mConfig,
+ mTexture, mBridge,
+ mCanvasElement,
+ mOffscreenCanvas)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasContext)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsICanvasRenderingContextInternal)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+// -
+
+CanvasContext::CanvasContext() = default;
+
+CanvasContext::~CanvasContext() {
+ Cleanup();
+ RemovePostRefreshObserver();
+}
+
+void CanvasContext::Cleanup() { Unconfigure(); }
+
+JSObject* CanvasContext::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GPUCanvasContext_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// -
+
+void CanvasContext::GetCanvas(
+ dom::OwningHTMLCanvasElementOrOffscreenCanvas& aRetVal) const {
+ if (mCanvasElement) {
+ aRetVal.SetAsHTMLCanvasElement() = mCanvasElement;
+ } else if (mOffscreenCanvas) {
+ aRetVal.SetAsOffscreenCanvas() = mOffscreenCanvas;
+ } else {
+ MOZ_CRASH(
+ "This should only happen briefly during CC Unlink, and no JS should "
+ "happen then.");
+ }
+}
+
+void CanvasContext::Configure(const dom::GPUCanvasConfiguration& aDesc) {
+ Unconfigure();
+
+ // these formats are guaranteed by the spec
+ switch (aDesc.mFormat) {
+ case dom::GPUTextureFormat::Rgba8unorm:
+ case dom::GPUTextureFormat::Rgba8unorm_srgb:
+ mGfxFormat = gfx::SurfaceFormat::R8G8B8A8;
+ break;
+ case dom::GPUTextureFormat::Bgra8unorm:
+ case dom::GPUTextureFormat::Bgra8unorm_srgb:
+ mGfxFormat = gfx::SurfaceFormat::B8G8R8A8;
+ break;
+ default:
+ NS_WARNING("Specified swap chain format is not supported");
+ return;
+ }
+
+ mConfig.reset(new dom::GPUCanvasConfiguration(aDesc));
+ mRemoteTextureOwnerId = Some(layers::RemoteTextureOwnerId::GetNext());
+ mTexture = aDesc.mDevice->InitSwapChain(aDesc, *mRemoteTextureOwnerId,
+ mGfxFormat, mCanvasSize);
+ if (!mTexture) {
+ Unconfigure();
+ return;
+ }
+
+ mTexture->mTargetContext = this;
+ mBridge = aDesc.mDevice->GetBridge();
+
+ ForceNewFrame();
+}
+
+void CanvasContext::Unconfigure() {
+ if (mBridge && mBridge->IsOpen() && mRemoteTextureOwnerId.isSome()) {
+ mBridge->SendSwapChainDestroy(*mRemoteTextureOwnerId);
+ }
+ mRemoteTextureOwnerId = Nothing();
+ mBridge = nullptr;
+ mConfig = nullptr;
+ mTexture = nullptr;
+ mGfxFormat = gfx::SurfaceFormat::UNKNOWN;
+}
+
+NS_IMETHODIMP CanvasContext::SetDimensions(int32_t aWidth, int32_t aHeight) {
+ aWidth = std::max(1, aWidth);
+ aHeight = std::max(1, aHeight);
+ const auto newSize = gfx::IntSize{aWidth, aHeight};
+ if (newSize == mCanvasSize) return NS_OK; // No-op no-change resizes.
+
+ mCanvasSize = newSize;
+ if (mConfig) {
+ const auto copy = dom::GPUCanvasConfiguration{
+ *mConfig}; // So we can't null it out on ourselves.
+ Configure(copy);
+ }
+ return NS_OK;
+}
+
+RefPtr<Texture> CanvasContext::GetCurrentTexture(ErrorResult& aRv) {
+ if (!mTexture) {
+ aRv.ThrowOperationError("Canvas not configured");
+ return nullptr;
+ }
+ return mTexture;
+}
+
+void CanvasContext::MaybeQueueSwapChainPresent() {
+ if (mPendingSwapChainPresent) {
+ return;
+ }
+
+ mPendingSwapChainPresent = true;
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(
+ NewCancelableRunnableMethod("CanvasContext::SwapChainPresent", this,
+ &CanvasContext::SwapChainPresent)));
+}
+
+void CanvasContext::SwapChainPresent() {
+ mPendingSwapChainPresent = false;
+ if (!mBridge || !mBridge->IsOpen() || mRemoteTextureOwnerId.isNothing() ||
+ !mTexture) {
+ return;
+ }
+ mLastRemoteTextureId = Some(layers::RemoteTextureId::GetNext());
+ mBridge->SwapChainPresent(mTexture->mId, *mLastRemoteTextureId,
+ *mRemoteTextureOwnerId);
+}
+
+bool CanvasContext::UpdateWebRenderCanvasData(
+ mozilla::nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) {
+ auto* renderer = aCanvasData->GetCanvasRenderer();
+
+ if (renderer && mRemoteTextureOwnerId.isSome() &&
+ renderer->GetRemoteTextureOwnerIdOfPushCallback() ==
+ mRemoteTextureOwnerId) {
+ return true;
+ }
+
+ renderer = aCanvasData->CreateCanvasRenderer();
+ if (!InitializeCanvasRenderer(aBuilder, renderer)) {
+ // Clear CanvasRenderer of WebRenderCanvasData
+ aCanvasData->ClearCanvasRenderer();
+ return false;
+ }
+ return true;
+}
+
+bool CanvasContext::InitializeCanvasRenderer(
+ nsDisplayListBuilder* aBuilder, layers::CanvasRenderer* aRenderer) {
+ if (mRemoteTextureOwnerId.isNothing()) {
+ return false;
+ }
+
+ layers::CanvasRendererData data;
+ data.mContext = this;
+ data.mSize = mCanvasSize;
+ data.mIsOpaque = false;
+ data.mRemoteTextureOwnerIdOfPushCallback = mRemoteTextureOwnerId;
+
+ aRenderer->Initialize(data);
+ aRenderer->SetDirty();
+ return true;
+}
+
+mozilla::UniquePtr<uint8_t[]> CanvasContext::GetImageBuffer(
+ int32_t* out_format, gfx::IntSize* out_imageSize) {
+ *out_format = 0;
+ *out_imageSize = {};
+
+ gfxAlphaType any;
+ RefPtr<gfx::SourceSurface> snapshot = GetSurfaceSnapshot(&any);
+ if (!snapshot) {
+ return nullptr;
+ }
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
+ *out_imageSize = dataSurface->GetSize();
+
+ if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
+ gfxUtils::GetImageBufferWithRandomNoise(
+ dataSurface,
+ /* aIsAlphaPremultiplied */ true, GetCookieJarSettings(), &*out_format);
+ }
+
+ return gfxUtils::GetImageBuffer(dataSurface, /* aIsAlphaPremultiplied */ true,
+ &*out_format);
+}
+
+NS_IMETHODIMP CanvasContext::GetInputStream(const char* aMimeType,
+ const nsAString& aEncoderOptions,
+ nsIInputStream** aStream) {
+ gfxAlphaType any;
+ RefPtr<gfx::SourceSurface> snapshot = GetSurfaceSnapshot(&any);
+ if (!snapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
+
+ if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
+ gfxUtils::GetInputStreamWithRandomNoise(
+ dataSurface, /* aIsAlphaPremultiplied */ true, aMimeType,
+ aEncoderOptions, GetCookieJarSettings(), aStream);
+ }
+
+ return gfxUtils::GetInputStream(dataSurface, /* aIsAlphaPremultiplied */ true,
+ aMimeType, aEncoderOptions, aStream);
+}
+
+already_AddRefed<mozilla::gfx::SourceSurface> CanvasContext::GetSurfaceSnapshot(
+ gfxAlphaType* aOutAlphaType) {
+ if (aOutAlphaType) {
+ *aOutAlphaType = gfxAlphaType::Premult;
+ }
+
+ auto* const cm = gfx::CanvasManagerChild::Get();
+ if (!cm) {
+ return nullptr;
+ }
+
+ if (!mBridge || !mBridge->IsOpen() || mRemoteTextureOwnerId.isNothing()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mRemoteTextureOwnerId.isSome());
+ return cm->GetSnapshot(cm->Id(), mBridge->Id(), mRemoteTextureOwnerId,
+ mGfxFormat, /* aPremultiply */ false,
+ /* aYFlip */ false);
+}
+
+void CanvasContext::ForceNewFrame() {
+ if (!mCanvasElement && !mOffscreenCanvas) {
+ return;
+ }
+
+ // Force a new frame to be built, which will execute the
+ // `CanvasContextType::WebGPU` switch case in `CreateWebRenderCommands` and
+ // populate the WR user data.
+ if (mCanvasElement) {
+ mCanvasElement->InvalidateCanvas();
+ } else if (mOffscreenCanvas) {
+ dom::OffscreenCanvasDisplayData data;
+ data.mSize = mCanvasSize;
+ data.mIsOpaque = false;
+ data.mOwnerId = mRemoteTextureOwnerId;
+ mOffscreenCanvas->UpdateDisplayData(data);
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/CanvasContext.h b/dom/webgpu/CanvasContext.h
new file mode 100644
index 0000000000..e9d3dba3c0
--- /dev/null
+++ b/dom/webgpu/CanvasContext.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_CanvasContext_H_
+#define GPU_CanvasContext_H_
+
+#include "nsICanvasRenderingContextInternal.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace dom {
+class OwningHTMLCanvasElementOrOffscreenCanvas;
+class Promise;
+struct GPUCanvasConfiguration;
+enum class GPUTextureFormat : uint8_t;
+} // namespace dom
+namespace webgpu {
+class Adapter;
+class Texture;
+
+class CanvasContext final : public nsICanvasRenderingContextInternal,
+ public nsWrapperCache {
+ private:
+ virtual ~CanvasContext();
+ void Cleanup();
+
+ public:
+ // nsISupports interface + CC
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(CanvasContext)
+
+ CanvasContext();
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ public: // nsICanvasRenderingContextInternal
+ int32_t GetWidth() override { return mCanvasSize.width; }
+ int32_t GetHeight() override { return mCanvasSize.height; }
+
+ NS_IMETHOD SetDimensions(int32_t aWidth, int32_t aHeight) override;
+ NS_IMETHOD InitializeWithDrawTarget(
+ nsIDocShell* aShell, NotNull<gfx::DrawTarget*> aTarget) override {
+ return NS_OK;
+ }
+
+ bool UpdateWebRenderCanvasData(mozilla::nsDisplayListBuilder* aBuilder,
+ WebRenderCanvasData* aCanvasData) override;
+
+ bool InitializeCanvasRenderer(nsDisplayListBuilder* aBuilder,
+ layers::CanvasRenderer* aRenderer) override;
+ mozilla::UniquePtr<uint8_t[]> GetImageBuffer(
+ int32_t* out_format, gfx::IntSize* out_imageSize) override;
+ NS_IMETHOD GetInputStream(const char* aMimeType,
+ const nsAString& aEncoderOptions,
+ nsIInputStream** aStream) override;
+ already_AddRefed<mozilla::gfx::SourceSurface> GetSurfaceSnapshot(
+ gfxAlphaType* aOutAlphaType) override;
+
+ void SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue) override {}
+ bool GetIsOpaque() override { return true; }
+
+ void ResetBitmap() override { Unconfigure(); }
+
+ void MarkContextClean() override {}
+
+ NS_IMETHOD Redraw(const gfxRect& aDirty) override { return NS_OK; }
+
+ void DidRefresh() override {}
+
+ void MarkContextCleanForFrameCapture() override {}
+ Watchable<FrameCaptureState>* GetFrameCaptureState() override {
+ return nullptr;
+ }
+
+ public:
+ void GetCanvas(dom::OwningHTMLCanvasElementOrOffscreenCanvas&) const;
+
+ void Configure(const dom::GPUCanvasConfiguration& aDesc);
+ void Unconfigure();
+
+ RefPtr<Texture> GetCurrentTexture(ErrorResult& aRv);
+ void MaybeQueueSwapChainPresent();
+ void SwapChainPresent();
+ void ForceNewFrame();
+
+ private:
+ gfx::IntSize mCanvasSize;
+ std::unique_ptr<dom::GPUCanvasConfiguration> mConfig;
+ bool mPendingSwapChainPresent = false;
+
+ RefPtr<WebGPUChild> mBridge;
+ RefPtr<Texture> mTexture;
+ gfx::SurfaceFormat mGfxFormat = gfx::SurfaceFormat::R8G8B8A8;
+
+ Maybe<layers::RemoteTextureId> mLastRemoteTextureId;
+ Maybe<layers::RemoteTextureOwnerId> mRemoteTextureOwnerId;
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_CanvasContext_H_
diff --git a/dom/webgpu/CommandBuffer.cpp b/dom/webgpu/CommandBuffer.cpp
new file mode 100644
index 0000000000..2ba8fd0420
--- /dev/null
+++ b/dom/webgpu/CommandBuffer.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "CommandBuffer.h"
+#include "ipc/WebGPUChild.h"
+
+#include "mozilla/webgpu/CanvasContext.h"
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(CommandBuffer, mParent)
+GPU_IMPL_JS_WRAP(CommandBuffer)
+
+CommandBuffer::CommandBuffer(Device* const aParent, RawId aId,
+ nsTArray<WeakPtr<CanvasContext>>&& aTargetContexts)
+ : ChildOf(aParent), mId(aId), mTargetContexts(std::move(aTargetContexts)) {
+ if (!aId) {
+ mValid = false;
+ }
+}
+
+CommandBuffer::~CommandBuffer() { Cleanup(); }
+
+void CommandBuffer::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendCommandBufferDestroy(mId);
+ }
+ }
+}
+
+Maybe<RawId> CommandBuffer::Commit() {
+ if (!mValid) {
+ return Nothing();
+ }
+ mValid = false;
+ for (const auto& targetContext : mTargetContexts) {
+ if (targetContext) {
+ targetContext->MaybeQueueSwapChainPresent();
+ }
+ }
+ return Some(mId);
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/CommandBuffer.h b/dom/webgpu/CommandBuffer.h
new file mode 100644
index 0000000000..be525d98f3
--- /dev/null
+++ b/dom/webgpu/CommandBuffer.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_CommandBuffer_H_
+#define GPU_CommandBuffer_H_
+
+#include "mozilla/WeakPtr.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+
+class CanvasContext;
+class Device;
+
+class CommandBuffer final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(CommandBuffer)
+ GPU_DECL_JS_WRAP(CommandBuffer)
+
+ CommandBuffer(Device* const aParent, RawId aId,
+ nsTArray<WeakPtr<CanvasContext>>&& aTargetContexts);
+
+ Maybe<RawId> Commit();
+
+ private:
+ CommandBuffer() = delete;
+ ~CommandBuffer();
+ void Cleanup();
+
+ const RawId mId;
+ const nsTArray<WeakPtr<CanvasContext>> mTargetContexts;
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_CommandBuffer_H_
diff --git a/dom/webgpu/CommandEncoder.cpp b/dom/webgpu/CommandEncoder.cpp
new file mode 100644
index 0000000000..45a8562a33
--- /dev/null
+++ b/dom/webgpu/CommandEncoder.cpp
@@ -0,0 +1,235 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "CommandEncoder.h"
+
+#include "CommandBuffer.h"
+#include "Buffer.h"
+#include "ComputePassEncoder.h"
+#include "Device.h"
+#include "RenderPassEncoder.h"
+#include "Utility.h"
+#include "mozilla/webgpu/CanvasContext.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "ipc/WebGPUChild.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(CommandEncoder, mParent, mBridge)
+GPU_IMPL_JS_WRAP(CommandEncoder)
+
+void CommandEncoder::ConvertTextureDataLayoutToFFI(
+ const dom::GPUImageDataLayout& aLayout,
+ ffi::WGPUImageDataLayout* aLayoutFFI) {
+ *aLayoutFFI = {};
+ aLayoutFFI->offset = aLayout.mOffset;
+
+ if (aLayout.mBytesPerRow.WasPassed()) {
+ aLayoutFFI->bytes_per_row = &aLayout.mBytesPerRow.Value();
+ } else {
+ aLayoutFFI->bytes_per_row = nullptr;
+ }
+
+ if (aLayout.mRowsPerImage.WasPassed()) {
+ aLayoutFFI->rows_per_image = &aLayout.mRowsPerImage.Value();
+ } else {
+ aLayoutFFI->rows_per_image = nullptr;
+ }
+}
+
+void CommandEncoder::ConvertTextureCopyViewToFFI(
+ const dom::GPUImageCopyTexture& aCopy,
+ ffi::WGPUImageCopyTexture* aViewFFI) {
+ *aViewFFI = {};
+ aViewFFI->texture = aCopy.mTexture->mId;
+ aViewFFI->mip_level = aCopy.mMipLevel;
+ if (aCopy.mOrigin.WasPassed()) {
+ const auto& origin = aCopy.mOrigin.Value();
+ if (origin.IsRangeEnforcedUnsignedLongSequence()) {
+ const auto& seq = origin.GetAsRangeEnforcedUnsignedLongSequence();
+ aViewFFI->origin.x = seq.Length() > 0 ? seq[0] : 0;
+ aViewFFI->origin.y = seq.Length() > 1 ? seq[1] : 0;
+ aViewFFI->origin.z = seq.Length() > 2 ? seq[2] : 0;
+ } else if (origin.IsGPUOrigin3DDict()) {
+ const auto& dict = origin.GetAsGPUOrigin3DDict();
+ aViewFFI->origin.x = dict.mX;
+ aViewFFI->origin.y = dict.mY;
+ aViewFFI->origin.z = dict.mZ;
+ } else {
+ MOZ_CRASH("Unexpected origin type");
+ }
+ }
+}
+
+static ffi::WGPUImageCopyTexture ConvertTextureCopyView(
+ const dom::GPUImageCopyTexture& aCopy) {
+ ffi::WGPUImageCopyTexture view = {};
+ CommandEncoder::ConvertTextureCopyViewToFFI(aCopy, &view);
+ return view;
+}
+
+CommandEncoder::CommandEncoder(Device* const aParent,
+ WebGPUChild* const aBridge, RawId aId)
+ : ChildOf(aParent), mId(aId), mBridge(aBridge) {}
+
+CommandEncoder::~CommandEncoder() { Cleanup(); }
+
+void CommandEncoder::Cleanup() {
+ if (mValid) {
+ mValid = false;
+ if (mBridge->IsOpen()) {
+ mBridge->SendCommandEncoderDestroy(mId);
+ }
+ }
+}
+
+void CommandEncoder::CopyBufferToBuffer(const Buffer& aSource,
+ BufferAddress aSourceOffset,
+ const Buffer& aDestination,
+ BufferAddress aDestinationOffset,
+ BufferAddress aSize) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ ffi::wgpu_command_encoder_copy_buffer_to_buffer(
+ aSource.mId, aSourceOffset, aDestination.mId, aDestinationOffset, aSize,
+ ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+ }
+}
+
+void CommandEncoder::CopyBufferToTexture(
+ const dom::GPUImageCopyBuffer& aSource,
+ const dom::GPUImageCopyTexture& aDestination,
+ const dom::GPUExtent3D& aCopySize) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ ffi::WGPUImageDataLayout src_layout = {};
+ CommandEncoder::ConvertTextureDataLayoutToFFI(aSource, &src_layout);
+ ffi::wgpu_command_encoder_copy_buffer_to_texture(
+ aSource.mBuffer->mId, &src_layout, ConvertTextureCopyView(aDestination),
+ ConvertExtent(aCopySize), ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+
+ const auto& targetContext = aDestination.mTexture->mTargetContext;
+ if (targetContext) {
+ mTargetContexts.AppendElement(targetContext);
+ }
+ }
+}
+void CommandEncoder::CopyTextureToBuffer(
+ const dom::GPUImageCopyTexture& aSource,
+ const dom::GPUImageCopyBuffer& aDestination,
+ const dom::GPUExtent3D& aCopySize) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ ffi::WGPUImageDataLayout dstLayout = {};
+ CommandEncoder::ConvertTextureDataLayoutToFFI(aDestination, &dstLayout);
+ ffi::wgpu_command_encoder_copy_texture_to_buffer(
+ ConvertTextureCopyView(aSource), aDestination.mBuffer->mId, &dstLayout,
+ ConvertExtent(aCopySize), ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+ }
+}
+void CommandEncoder::CopyTextureToTexture(
+ const dom::GPUImageCopyTexture& aSource,
+ const dom::GPUImageCopyTexture& aDestination,
+ const dom::GPUExtent3D& aCopySize) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ ffi::wgpu_command_encoder_copy_texture_to_texture(
+ ConvertTextureCopyView(aSource), ConvertTextureCopyView(aDestination),
+ ConvertExtent(aCopySize), ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+
+ const auto& targetContext = aDestination.mTexture->mTargetContext;
+ if (targetContext) {
+ mTargetContexts.AppendElement(targetContext);
+ }
+ }
+}
+
+void CommandEncoder::PushDebugGroup(const nsAString& aString) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ NS_ConvertUTF16toUTF8 marker(aString);
+ ffi::wgpu_command_encoder_push_debug_group(&marker, ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+ }
+}
+void CommandEncoder::PopDebugGroup() {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ ffi::wgpu_command_encoder_pop_debug_group(ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+ }
+}
+void CommandEncoder::InsertDebugMarker(const nsAString& aString) {
+ if (mValid && mBridge->IsOpen()) {
+ ipc::ByteBuf bb;
+ NS_ConvertUTF16toUTF8 marker(aString);
+ ffi::wgpu_command_encoder_insert_debug_marker(&marker, ToFFI(&bb));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb));
+ }
+}
+
+already_AddRefed<ComputePassEncoder> CommandEncoder::BeginComputePass(
+ const dom::GPUComputePassDescriptor& aDesc) {
+ RefPtr<ComputePassEncoder> pass = new ComputePassEncoder(this, aDesc);
+ return pass.forget();
+}
+
+already_AddRefed<RenderPassEncoder> CommandEncoder::BeginRenderPass(
+ const dom::GPURenderPassDescriptor& aDesc) {
+ for (const auto& at : aDesc.mColorAttachments) {
+ auto* targetContext = at.mView->GetTargetContext();
+ if (targetContext) {
+ mTargetContexts.AppendElement(targetContext);
+ }
+ if (at.mResolveTarget.WasPassed()) {
+ targetContext = at.mResolveTarget.Value().GetTargetContext();
+ mTargetContexts.AppendElement(targetContext);
+ }
+ }
+
+ RefPtr<RenderPassEncoder> pass = new RenderPassEncoder(this, aDesc);
+ return pass.forget();
+}
+
+void CommandEncoder::EndComputePass(ffi::WGPUComputePass& aPass,
+ ErrorResult& aRv) {
+ if (!mValid || !mBridge->IsOpen()) {
+ return aRv.ThrowInvalidStateError("Command encoder is not valid");
+ }
+
+ ipc::ByteBuf byteBuf;
+ ffi::wgpu_compute_pass_finish(&aPass, ToFFI(&byteBuf));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(byteBuf));
+}
+
+void CommandEncoder::EndRenderPass(ffi::WGPURenderPass& aPass,
+ ErrorResult& aRv) {
+ if (!mValid || !mBridge->IsOpen()) {
+ return aRv.ThrowInvalidStateError("Command encoder is not valid");
+ }
+
+ ipc::ByteBuf byteBuf;
+ ffi::wgpu_render_pass_finish(&aPass, ToFFI(&byteBuf));
+ mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(byteBuf));
+}
+
+already_AddRefed<CommandBuffer> CommandEncoder::Finish(
+ const dom::GPUCommandBufferDescriptor& aDesc) {
+ RawId id = 0;
+ if (mValid && mBridge->IsOpen()) {
+ mValid = false;
+ id = mBridge->CommandEncoderFinish(mId, mParent->mId, aDesc);
+ }
+ RefPtr<CommandBuffer> comb =
+ new CommandBuffer(mParent, id, std::move(mTargetContexts));
+ return comb.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/CommandEncoder.h b/dom/webgpu/CommandEncoder.h
new file mode 100644
index 0000000000..fc58b7ee35
--- /dev/null
+++ b/dom/webgpu/CommandEncoder.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_CommandEncoder_H_
+#define GPU_CommandEncoder_H_
+
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+struct GPUComputePassDescriptor;
+template <typename T>
+class Sequence;
+struct GPUCommandBufferDescriptor;
+class GPUComputePipelineOrGPURenderPipeline;
+class RangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+struct GPUImageCopyBuffer;
+struct GPUImageCopyTexture;
+struct GPUImageBitmapCopyView;
+struct GPUImageDataLayout;
+struct GPURenderPassDescriptor;
+using GPUExtent3D = RangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+} // namespace dom
+namespace webgpu {
+namespace ffi {
+struct WGPUComputePass;
+struct WGPURenderPass;
+struct WGPUImageDataLayout;
+struct WGPUImageCopyTexture_TextureId;
+struct WGPUExtent3d;
+} // namespace ffi
+
+class BindGroup;
+class Buffer;
+class CanvasContext;
+class CommandBuffer;
+class ComputePassEncoder;
+class Device;
+class RenderPassEncoder;
+
+class CommandEncoder final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(CommandEncoder)
+ GPU_DECL_JS_WRAP(CommandEncoder)
+
+ CommandEncoder(Device* const aParent, WebGPUChild* const aBridge, RawId aId);
+
+ const RawId mId;
+
+ static void ConvertTextureDataLayoutToFFI(
+ const dom::GPUImageDataLayout& aLayout,
+ ffi::WGPUImageDataLayout* aLayoutFFI);
+ static void ConvertTextureCopyViewToFFI(
+ const dom::GPUImageCopyTexture& aCopy,
+ ffi::WGPUImageCopyTexture_TextureId* aViewFFI);
+
+ private:
+ ~CommandEncoder();
+ void Cleanup();
+
+ RefPtr<WebGPUChild> mBridge;
+ nsTArray<WeakPtr<CanvasContext>> mTargetContexts;
+
+ public:
+ const auto& GetDevice() const { return mParent; };
+
+ void EndComputePass(ffi::WGPUComputePass& aPass, ErrorResult& aRv);
+ void EndRenderPass(ffi::WGPURenderPass& aPass, ErrorResult& aRv);
+
+ void CopyBufferToBuffer(const Buffer& aSource, BufferAddress aSourceOffset,
+ const Buffer& aDestination,
+ BufferAddress aDestinationOffset,
+ BufferAddress aSize);
+ void CopyBufferToTexture(const dom::GPUImageCopyBuffer& aSource,
+ const dom::GPUImageCopyTexture& aDestination,
+ const dom::GPUExtent3D& aCopySize);
+ void CopyTextureToBuffer(const dom::GPUImageCopyTexture& aSource,
+ const dom::GPUImageCopyBuffer& aDestination,
+ const dom::GPUExtent3D& aCopySize);
+ void CopyTextureToTexture(const dom::GPUImageCopyTexture& aSource,
+ const dom::GPUImageCopyTexture& aDestination,
+ const dom::GPUExtent3D& aCopySize);
+
+ void PushDebugGroup(const nsAString& aString);
+ void PopDebugGroup();
+ void InsertDebugMarker(const nsAString& aString);
+
+ already_AddRefed<ComputePassEncoder> BeginComputePass(
+ const dom::GPUComputePassDescriptor& aDesc);
+ already_AddRefed<RenderPassEncoder> BeginRenderPass(
+ const dom::GPURenderPassDescriptor& aDesc);
+ already_AddRefed<CommandBuffer> Finish(
+ const dom::GPUCommandBufferDescriptor& aDesc);
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_CommandEncoder_H_
diff --git a/dom/webgpu/CompilationInfo.cpp b/dom/webgpu/CompilationInfo.cpp
new file mode 100644
index 0000000000..6f8ebf5490
--- /dev/null
+++ b/dom/webgpu/CompilationInfo.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompilationInfo.h"
+#include "CompilationMessage.h"
+#include "ShaderModule.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(CompilationInfo, mParent)
+GPU_IMPL_JS_WRAP(CompilationInfo)
+
+CompilationInfo::CompilationInfo(ShaderModule* const aParent)
+ : ChildOf(aParent) {}
+
+void CompilationInfo::SetMessages(
+ nsTArray<mozilla::webgpu::WebGPUCompilationMessage>& aMessages) {
+ for (auto& msg : aMessages) {
+ mMessages.AppendElement(MakeAndAddRef<mozilla::webgpu::CompilationMessage>(
+ this, msg.lineNum, msg.linePos, msg.offset, std::move(msg.message)));
+ }
+}
+
+void CompilationInfo::GetMessages(
+ nsTArray<RefPtr<mozilla::webgpu::CompilationMessage>>& aMessages) {
+ for (auto& msg : mMessages) {
+ aMessages.AppendElement(msg);
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/CompilationInfo.h b/dom/webgpu/CompilationInfo.h
new file mode 100644
index 0000000000..38d687cf25
--- /dev/null
+++ b/dom/webgpu/CompilationInfo.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_CompilationInfo_H_
+#define GPU_CompilationInfo_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "CompilationMessage.h"
+
+namespace mozilla::webgpu {
+class ShaderModule;
+
+class CompilationInfo final : public nsWrapperCache,
+ public ChildOf<ShaderModule> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(CompilationInfo)
+ GPU_DECL_JS_WRAP(CompilationInfo)
+
+ explicit CompilationInfo(ShaderModule* const aParent);
+
+ void SetMessages(
+ nsTArray<mozilla::webgpu::WebGPUCompilationMessage>& aMessages);
+
+ void GetMessages(
+ nsTArray<RefPtr<mozilla::webgpu::CompilationMessage>>& aMessages);
+
+ private:
+ ~CompilationInfo() = default;
+ void Cleanup() {}
+
+ nsTArray<RefPtr<mozilla::webgpu::CompilationMessage>> mMessages;
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_CompilationInfo_H_
diff --git a/dom/webgpu/CompilationMessage.cpp b/dom/webgpu/CompilationMessage.cpp
new file mode 100644
index 0000000000..f0df8c1db1
--- /dev/null
+++ b/dom/webgpu/CompilationMessage.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompilationMessage.h"
+#include "CompilationInfo.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(CompilationMessage, mParent)
+GPU_IMPL_JS_WRAP(CompilationMessage)
+
+CompilationMessage::CompilationMessage(CompilationInfo* const aParent,
+ uint64_t aLineNum, uint64_t aLinePos,
+ uint64_t aOffset, nsString&& aMessage)
+ : ChildOf(aParent),
+ mLineNum(aLineNum),
+ mLinePos(aLinePos),
+ mOffset(aOffset),
+ mMessage(std::move(aMessage)) {}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/CompilationMessage.h b/dom/webgpu/CompilationMessage.h
new file mode 100644
index 0000000000..685a41f68e
--- /dev/null
+++ b/dom/webgpu/CompilationMessage.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_CompilationMessage_H_
+#define GPU_CompilationMessage_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla {
+namespace dom {
+class DOMString;
+} // namespace dom
+namespace webgpu {
+class CompilationInfo;
+
+class CompilationMessage final : public nsWrapperCache,
+ public ChildOf<CompilationInfo> {
+ dom::GPUCompilationMessageType mType = dom::GPUCompilationMessageType::Error;
+ uint64_t mLineNum = 0;
+ uint64_t mLinePos = 0;
+ uint64_t mOffset = 0;
+ uint64_t mLength = 0;
+ nsString mMessage;
+
+ public:
+ GPU_DECL_CYCLE_COLLECTION(CompilationMessage)
+ GPU_DECL_JS_WRAP(CompilationMessage)
+
+ explicit CompilationMessage(CompilationInfo* const aParent, uint64_t aLineNum,
+ uint64_t aLinePos, uint64_t aOffset,
+ nsString&& aMessage);
+
+ void GetMessage(dom::DOMString& aMessage) {
+ aMessage.AsAString().Assign(mMessage);
+ }
+ dom::GPUCompilationMessageType Type() const { return mType; }
+ uint64_t LineNum() const { return mLineNum; }
+ uint64_t LinePos() const { return mLinePos; }
+ uint64_t Offset() const { return mOffset; }
+ uint64_t Length() const { return mLength; }
+
+ private:
+ ~CompilationMessage() = default;
+ void Cleanup() {}
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_CompilationMessage_H_
diff --git a/dom/webgpu/ComputePassEncoder.cpp b/dom/webgpu/ComputePassEncoder.cpp
new file mode 100644
index 0000000000..eb9fc8e840
--- /dev/null
+++ b/dom/webgpu/ComputePassEncoder.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "ComputePassEncoder.h"
+#include "BindGroup.h"
+#include "ComputePipeline.h"
+#include "CommandEncoder.h"
+
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(ComputePassEncoder, mParent, mUsedBindGroups,
+ mUsedPipelines)
+GPU_IMPL_JS_WRAP(ComputePassEncoder)
+
+ffi::WGPUComputePass* ScopedFfiComputeTraits::empty() { return nullptr; }
+
+void ScopedFfiComputeTraits::release(ffi::WGPUComputePass* raw) {
+ if (raw) {
+ ffi::wgpu_compute_pass_destroy(raw);
+ }
+}
+
+ffi::WGPUComputePass* BeginComputePass(
+ RawId aEncoderId, const dom::GPUComputePassDescriptor& aDesc) {
+ ffi::WGPUComputePassDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ return ffi::wgpu_command_encoder_begin_compute_pass(aEncoderId, &desc);
+}
+
+ComputePassEncoder::ComputePassEncoder(
+ CommandEncoder* const aParent, const dom::GPUComputePassDescriptor& aDesc)
+ : ChildOf(aParent), mPass(BeginComputePass(aParent->mId, aDesc)) {}
+
+ComputePassEncoder::~ComputePassEncoder() {
+ if (mValid) {
+ mValid = false;
+ }
+}
+
+void ComputePassEncoder::SetBindGroup(
+ uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets) {
+ if (mValid) {
+ mUsedBindGroups.AppendElement(&aBindGroup);
+ ffi::wgpu_compute_pass_set_bind_group(mPass, aSlot, aBindGroup.mId,
+ aDynamicOffsets.Elements(),
+ aDynamicOffsets.Length());
+ }
+}
+
+void ComputePassEncoder::SetPipeline(const ComputePipeline& aPipeline) {
+ if (mValid) {
+ mUsedPipelines.AppendElement(&aPipeline);
+ ffi::wgpu_compute_pass_set_pipeline(mPass, aPipeline.mId);
+ }
+}
+
+void ComputePassEncoder::DispatchWorkgroups(uint32_t x, uint32_t y,
+ uint32_t z) {
+ if (mValid) {
+ ffi::wgpu_compute_pass_dispatch_workgroups(mPass, x, y, z);
+ }
+}
+
+void ComputePassEncoder::DispatchWorkgroupsIndirect(
+ const Buffer& aIndirectBuffer, uint64_t aIndirectOffset) {
+ if (mValid) {
+ ffi::wgpu_compute_pass_dispatch_workgroups_indirect(
+ mPass, aIndirectBuffer.mId, aIndirectOffset);
+ }
+}
+
+void ComputePassEncoder::PushDebugGroup(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_compute_pass_push_debug_group(mPass, utf8.get(), 0);
+ }
+}
+void ComputePassEncoder::PopDebugGroup() {
+ if (mValid) {
+ ffi::wgpu_compute_pass_pop_debug_group(mPass);
+ }
+}
+void ComputePassEncoder::InsertDebugMarker(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_compute_pass_insert_debug_marker(mPass, utf8.get(), 0);
+ }
+}
+
+void ComputePassEncoder::End(ErrorResult& aRv) {
+ if (mValid) {
+ mValid = false;
+ auto* pass = mPass.forget();
+ MOZ_ASSERT(pass);
+ mParent->EndComputePass(*pass, aRv);
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ComputePassEncoder.h b/dom/webgpu/ComputePassEncoder.h
new file mode 100644
index 0000000000..76c0d87589
--- /dev/null
+++ b/dom/webgpu/ComputePassEncoder.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_ComputePassEncoder_H_
+#define GPU_ComputePassEncoder_H_
+
+#include "mozilla/Scoped.h"
+#include "mozilla/dom/TypedArray.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+struct GPUComputePassDescriptor;
+}
+
+namespace webgpu {
+namespace ffi {
+struct WGPUComputePass;
+} // namespace ffi
+
+class BindGroup;
+class Buffer;
+class CommandEncoder;
+class ComputePipeline;
+
+struct ScopedFfiComputeTraits {
+ using type = ffi::WGPUComputePass*;
+ static type empty();
+ static void release(type raw);
+};
+
+class ComputePassEncoder final : public ObjectBase,
+ public ChildOf<CommandEncoder> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(ComputePassEncoder)
+ GPU_DECL_JS_WRAP(ComputePassEncoder)
+
+ ComputePassEncoder(CommandEncoder* const aParent,
+ const dom::GPUComputePassDescriptor& aDesc);
+
+ private:
+ virtual ~ComputePassEncoder();
+ void Cleanup() {}
+
+ Scoped<ScopedFfiComputeTraits> mPass;
+ // keep all the used objects alive while the pass is recorded
+ nsTArray<RefPtr<const BindGroup>> mUsedBindGroups;
+ nsTArray<RefPtr<const ComputePipeline>> mUsedPipelines;
+
+ public:
+ // programmable pass encoder
+ void SetBindGroup(uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets);
+ // self
+ void SetPipeline(const ComputePipeline& aPipeline);
+
+ void DispatchWorkgroups(uint32_t x, uint32_t y, uint32_t z);
+ void DispatchWorkgroupsIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset);
+
+ void PushDebugGroup(const nsAString& aString);
+ void PopDebugGroup();
+ void InsertDebugMarker(const nsAString& aString);
+
+ void End(ErrorResult& aRv);
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_ComputePassEncoder_H_
diff --git a/dom/webgpu/ComputePipeline.cpp b/dom/webgpu/ComputePipeline.cpp
new file mode 100644
index 0000000000..53a8031871
--- /dev/null
+++ b/dom/webgpu/ComputePipeline.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ComputePipeline.h"
+
+#include "Device.h"
+#include "ipc/WebGPUChild.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(ComputePipeline, mParent)
+GPU_IMPL_JS_WRAP(ComputePipeline)
+
+ComputePipeline::ComputePipeline(Device* const aParent, RawId aId,
+ RawId aImplicitPipelineLayoutId,
+ nsTArray<RawId>&& aImplicitBindGroupLayoutIds)
+ : ChildOf(aParent),
+ mImplicitPipelineLayoutId(aImplicitPipelineLayoutId),
+ mImplicitBindGroupLayoutIds(std::move(aImplicitBindGroupLayoutIds)),
+ mId(aId) {}
+
+ComputePipeline::~ComputePipeline() { Cleanup(); }
+
+void ComputePipeline::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendComputePipelineDestroy(mId);
+ if (mImplicitPipelineLayoutId) {
+ bridge->SendImplicitLayoutDestroy(mImplicitPipelineLayoutId,
+ mImplicitBindGroupLayoutIds);
+ }
+ }
+ }
+}
+
+already_AddRefed<BindGroupLayout> ComputePipeline::GetBindGroupLayout(
+ uint32_t index) const {
+ const RawId id = index < mImplicitBindGroupLayoutIds.Length()
+ ? mImplicitBindGroupLayoutIds[index]
+ : 0;
+ RefPtr<BindGroupLayout> object = new BindGroupLayout(mParent, id, false);
+ return object.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ComputePipeline.h b/dom/webgpu/ComputePipeline.h
new file mode 100644
index 0000000000..5dbd972912
--- /dev/null
+++ b/dom/webgpu/ComputePipeline.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_ComputePipeline_H_
+#define GPU_ComputePipeline_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsTArray.h"
+
+namespace mozilla::webgpu {
+
+class BindGroupLayout;
+class Device;
+
+class ComputePipeline final : public ObjectBase, public ChildOf<Device> {
+ const RawId mImplicitPipelineLayoutId;
+ const nsTArray<RawId> mImplicitBindGroupLayoutIds;
+
+ public:
+ GPU_DECL_CYCLE_COLLECTION(ComputePipeline)
+ GPU_DECL_JS_WRAP(ComputePipeline)
+
+ const RawId mId;
+
+ ComputePipeline(Device* const aParent, RawId aId,
+ RawId aImplicitPipelineLayoutId,
+ nsTArray<RawId>&& aImplicitBindGroupLayoutIds);
+ already_AddRefed<BindGroupLayout> GetBindGroupLayout(uint32_t index) const;
+
+ private:
+ ~ComputePipeline();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_ComputePipeline_H_
diff --git a/dom/webgpu/Device.cpp b/dom/webgpu/Device.cpp
new file mode 100644
index 0000000000..07411154e0
--- /dev/null
+++ b/dom/webgpu/Device.cpp
@@ -0,0 +1,371 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "js/ArrayBuffer.h"
+#include "js/Value.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "Device.h"
+#include "CommandEncoder.h"
+#include "BindGroup.h"
+
+#include "Adapter.h"
+#include "Buffer.h"
+#include "ComputePipeline.h"
+#include "DeviceLostInfo.h"
+#include "PipelineLayout.h"
+#include "Queue.h"
+#include "RenderBundleEncoder.h"
+#include "RenderPipeline.h"
+#include "Sampler.h"
+#include "SupportedFeatures.h"
+#include "SupportedLimits.h"
+#include "Texture.h"
+#include "TextureView.h"
+#include "ValidationError.h"
+#include "ipc/WebGPUChild.h"
+
+namespace mozilla::webgpu {
+
+mozilla::LazyLogModule gWebGPULog("WebGPU");
+
+GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(Device, DOMEventTargetHelper,
+ mBridge, mQueue, mFeatures,
+ mLimits, mLostPromise);
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(Device, DOMEventTargetHelper)
+GPU_IMPL_JS_WRAP(Device)
+
+RefPtr<WebGPUChild> Device::GetBridge() { return mBridge; }
+
+Device::Device(Adapter* const aParent, RawId aId,
+ UniquePtr<ffi::WGPULimits> aRawLimits)
+ : DOMEventTargetHelper(aParent->GetParentObject()),
+ mId(aId),
+ // features are filled in Adapter::RequestDevice
+ mFeatures(new SupportedFeatures(aParent)),
+ mLimits(new SupportedLimits(aParent, std::move(aRawLimits))),
+ mBridge(aParent->mBridge),
+ mQueue(new class Queue(this, aParent->mBridge, aId)) {
+ mBridge->RegisterDevice(this);
+}
+
+Device::~Device() { Cleanup(); }
+
+void Device::Cleanup() {
+ if (!mValid) {
+ return;
+ }
+
+ mValid = false;
+
+ if (mBridge) {
+ mBridge->UnregisterDevice(mId);
+ }
+
+ // Cycle collection may have disconnected the promise object.
+ if (mLostPromise && mLostPromise->PromiseObj() != nullptr) {
+ auto info = MakeRefPtr<DeviceLostInfo>(GetParentObject(),
+ dom::GPUDeviceLostReason::Destroyed,
+ u"Device destroyed"_ns);
+ mLostPromise->MaybeResolve(info);
+ }
+}
+
+void Device::CleanupUnregisteredInParent() {
+ if (mBridge) {
+ mBridge->FreeUnregisteredInParentDevice(mId);
+ }
+ mValid = false;
+}
+
+bool Device::IsLost() const { return !mBridge || !mBridge->CanSend(); }
+
+// Generate an error on the Device timeline for this device.
+//
+// aMessage is interpreted as UTF-8.
+void Device::GenerateError(const nsCString& aMessage) {
+ if (mBridge->CanSend()) {
+ mBridge->SendGenerateError(mId, aMessage);
+ }
+}
+
+void Device::GetLabel(nsAString& aValue) const { aValue = mLabel; }
+void Device::SetLabel(const nsAString& aLabel) { mLabel = aLabel; }
+
+dom::Promise* Device::GetLost(ErrorResult& aRv) {
+ if (!mLostPromise) {
+ mLostPromise = dom::Promise::Create(GetParentObject(), aRv);
+ if (mLostPromise && !mBridge->CanSend()) {
+ auto info = MakeRefPtr<DeviceLostInfo>(GetParentObject(),
+ u"WebGPUChild destroyed"_ns);
+ mLostPromise->MaybeResolve(info);
+ }
+ }
+ return mLostPromise;
+}
+
+already_AddRefed<Buffer> Device::CreateBuffer(
+ const dom::GPUBufferDescriptor& aDesc, ErrorResult& aRv) {
+ return Buffer::Create(this, mId, aDesc, aRv);
+}
+
+already_AddRefed<Texture> Device::CreateTexture(
+ const dom::GPUTextureDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateTexture(mId, aDesc);
+ }
+ RefPtr<Texture> texture = new Texture(this, id, aDesc);
+ return texture.forget();
+}
+
+already_AddRefed<Sampler> Device::CreateSampler(
+ const dom::GPUSamplerDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateSampler(mId, aDesc);
+ }
+ RefPtr<Sampler> sampler = new Sampler(this, id);
+ return sampler.forget();
+}
+
+already_AddRefed<CommandEncoder> Device::CreateCommandEncoder(
+ const dom::GPUCommandEncoderDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateCommandEncoder(mId, aDesc);
+ }
+ RefPtr<CommandEncoder> encoder = new CommandEncoder(this, mBridge, id);
+ return encoder.forget();
+}
+
+already_AddRefed<RenderBundleEncoder> Device::CreateRenderBundleEncoder(
+ const dom::GPURenderBundleEncoderDescriptor& aDesc) {
+ RefPtr<RenderBundleEncoder> encoder =
+ new RenderBundleEncoder(this, mBridge, aDesc);
+ return encoder.forget();
+}
+
+already_AddRefed<BindGroupLayout> Device::CreateBindGroupLayout(
+ const dom::GPUBindGroupLayoutDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateBindGroupLayout(mId, aDesc);
+ }
+ RefPtr<BindGroupLayout> object = new BindGroupLayout(this, id, true);
+ return object.forget();
+}
+already_AddRefed<PipelineLayout> Device::CreatePipelineLayout(
+ const dom::GPUPipelineLayoutDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreatePipelineLayout(mId, aDesc);
+ }
+ RefPtr<PipelineLayout> object = new PipelineLayout(this, id);
+ return object.forget();
+}
+already_AddRefed<BindGroup> Device::CreateBindGroup(
+ const dom::GPUBindGroupDescriptor& aDesc) {
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateBindGroup(mId, aDesc);
+ }
+ RefPtr<BindGroup> object = new BindGroup(this, id);
+ return object.forget();
+}
+
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION already_AddRefed<ShaderModule>
+Device::CreateShaderModule(JSContext* aCx,
+ const dom::GPUShaderModuleDescriptor& aDesc) {
+ Unused << aCx;
+
+ if (!mBridge->CanSend()) {
+ return nullptr;
+ }
+
+ ErrorResult err;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), err);
+ if (NS_WARN_IF(err.Failed())) {
+ return nullptr;
+ }
+
+ return MOZ_KnownLive(mBridge)->DeviceCreateShaderModule(*this, aDesc,
+ promise);
+}
+
+already_AddRefed<ComputePipeline> Device::CreateComputePipeline(
+ const dom::GPUComputePipelineDescriptor& aDesc) {
+ PipelineCreationContext context = {mId};
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateComputePipeline(&context, aDesc);
+ }
+ RefPtr<ComputePipeline> object =
+ new ComputePipeline(this, id, context.mImplicitPipelineLayoutId,
+ std::move(context.mImplicitBindGroupLayoutIds));
+ return object.forget();
+}
+
+already_AddRefed<RenderPipeline> Device::CreateRenderPipeline(
+ const dom::GPURenderPipelineDescriptor& aDesc) {
+ PipelineCreationContext context = {mId};
+ RawId id = 0;
+ if (mBridge->CanSend()) {
+ id = mBridge->DeviceCreateRenderPipeline(&context, aDesc);
+ }
+ RefPtr<RenderPipeline> object =
+ new RenderPipeline(this, id, context.mImplicitPipelineLayoutId,
+ std::move(context.mImplicitBindGroupLayoutIds));
+ return object.forget();
+}
+
+already_AddRefed<dom::Promise> Device::CreateComputePipelineAsync(
+ const dom::GPUComputePipelineDescriptor& aDesc, ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (!mBridge->CanSend()) {
+ promise->MaybeRejectWithOperationError("Internal communication error");
+ return promise.forget();
+ }
+
+ std::shared_ptr<PipelineCreationContext> context(
+ new PipelineCreationContext());
+ context->mParentId = mId;
+ mBridge->DeviceCreateComputePipelineAsync(context.get(), aDesc)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, context, promise](RawId aId) {
+ RefPtr<ComputePipeline> object = new ComputePipeline(
+ self, aId, context->mImplicitPipelineLayoutId,
+ std::move(context->mImplicitBindGroupLayoutIds));
+ promise->MaybeResolve(object);
+ },
+ [promise](const ipc::ResponseRejectReason&) {
+ promise->MaybeRejectWithOperationError(
+ "Internal communication error");
+ });
+
+ return promise.forget();
+}
+
+already_AddRefed<dom::Promise> Device::CreateRenderPipelineAsync(
+ const dom::GPURenderPipelineDescriptor& aDesc, ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (!mBridge->CanSend()) {
+ promise->MaybeRejectWithOperationError("Internal communication error");
+ return promise.forget();
+ }
+
+ std::shared_ptr<PipelineCreationContext> context(
+ new PipelineCreationContext());
+ context->mParentId = mId;
+ mBridge->DeviceCreateRenderPipelineAsync(context.get(), aDesc)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, context, promise](RawId aId) {
+ RefPtr<RenderPipeline> object = new RenderPipeline(
+ self, aId, context->mImplicitPipelineLayoutId,
+ std::move(context->mImplicitBindGroupLayoutIds));
+ promise->MaybeResolve(object);
+ },
+ [promise](const ipc::ResponseRejectReason&) {
+ promise->MaybeRejectWithOperationError(
+ "Internal communication error");
+ });
+
+ return promise.forget();
+}
+
+already_AddRefed<Texture> Device::InitSwapChain(
+ const dom::GPUCanvasConfiguration& aDesc,
+ const layers::RemoteTextureOwnerId aOwnerId, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aCanvasSize) {
+ if (!mBridge->CanSend()) {
+ return nullptr;
+ }
+
+ const layers::RGBDescriptor rgbDesc(aCanvasSize, aFormat);
+ // buffer count doesn't matter much, will be created on demand
+ const size_t maxBufferCount = 10;
+ mBridge->DeviceCreateSwapChain(mId, rgbDesc, maxBufferCount, aOwnerId);
+
+ dom::GPUTextureDescriptor desc;
+ desc.mDimension = dom::GPUTextureDimension::_2d;
+ auto& sizeDict = desc.mSize.SetAsGPUExtent3DDict();
+ sizeDict.mWidth = aCanvasSize.width;
+ sizeDict.mHeight = aCanvasSize.height;
+ sizeDict.mDepthOrArrayLayers = 1;
+ desc.mFormat = aDesc.mFormat;
+ desc.mMipLevelCount = 1;
+ desc.mSampleCount = 1;
+ desc.mUsage = aDesc.mUsage | dom::GPUTextureUsage_Binding::COPY_SRC;
+ return CreateTexture(desc);
+}
+
+bool Device::CheckNewWarning(const nsACString& aMessage) {
+ return mKnownWarnings.EnsureInserted(aMessage);
+}
+
+void Device::Destroy() {
+ // TODO
+}
+
+void Device::PushErrorScope(const dom::GPUErrorFilter& aFilter) {
+ if (mBridge->CanSend()) {
+ mBridge->SendDevicePushErrorScope(mId);
+ }
+}
+
+already_AddRefed<dom::Promise> Device::PopErrorScope(ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (!mBridge->CanSend()) {
+ promise->MaybeRejectWithOperationError("Internal communication error");
+ return promise.forget();
+ }
+
+ auto errorPromise = mBridge->SendDevicePopErrorScope(mId);
+
+ errorPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, promise](const MaybeScopedError& aMaybeError) {
+ if (aMaybeError) {
+ if (aMaybeError->operationError) {
+ promise->MaybeRejectWithOperationError("Stack is empty");
+ } else {
+ dom::OwningGPUOutOfMemoryErrorOrGPUValidationError error;
+ if (aMaybeError->validationMessage.IsEmpty()) {
+ error.SetAsGPUOutOfMemoryError();
+ } else {
+ error.SetAsGPUValidationError() = new ValidationError(
+ self->GetParentObject(), aMaybeError->validationMessage);
+ }
+ promise->MaybeResolve(std::move(error));
+ }
+ } else {
+ promise->MaybeResolveWithUndefined();
+ }
+ },
+ [promise](const ipc::ResponseRejectReason&) {
+ promise->MaybeRejectWithOperationError("Internal communication error");
+ });
+
+ return promise.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Device.h b/dom/webgpu/Device.h
new file mode 100644
index 0000000000..a0744c6805
--- /dev/null
+++ b/dom/webgpu/Device.h
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_DEVICE_H_
+#define GPU_DEVICE_H_
+
+#include "ObjectModel.h"
+#include "nsTHashSet.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "mozilla/webgpu/PWebGPUTypes.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/DOMEventTargetHelper.h"
+
+namespace mozilla {
+namespace dom {
+struct GPUExtensions;
+struct GPUFeatures;
+struct GPULimits;
+struct GPUExtent3DDict;
+
+struct GPUBufferDescriptor;
+struct GPUTextureDescriptor;
+struct GPUSamplerDescriptor;
+struct GPUBindGroupLayoutDescriptor;
+struct GPUPipelineLayoutDescriptor;
+struct GPUBindGroupDescriptor;
+struct GPUBlendStateDescriptor;
+struct GPUDepthStencilStateDescriptor;
+struct GPUInputStateDescriptor;
+struct GPUShaderModuleDescriptor;
+struct GPUAttachmentStateDescriptor;
+struct GPUComputePipelineDescriptor;
+struct GPURenderBundleEncoderDescriptor;
+struct GPURenderPipelineDescriptor;
+struct GPUCommandEncoderDescriptor;
+struct GPUCanvasConfiguration;
+
+class EventHandlerNonNull;
+class Promise;
+template <typename T>
+class Sequence;
+class GPUBufferOrGPUTexture;
+enum class GPUErrorFilter : uint8_t;
+enum class GPUFeatureName : uint8_t;
+class GPULogCallback;
+} // namespace dom
+namespace ipc {
+enum class ResponseRejectReason;
+} // namespace ipc
+
+namespace webgpu {
+namespace ffi {
+struct WGPULimits;
+}
+class Adapter;
+class BindGroup;
+class BindGroupLayout;
+class Buffer;
+class CommandEncoder;
+class ComputePipeline;
+class Fence;
+class InputState;
+class PipelineLayout;
+class Queue;
+class RenderBundleEncoder;
+class RenderPipeline;
+class Sampler;
+class ShaderModule;
+class SupportedFeatures;
+class SupportedLimits;
+class Texture;
+class WebGPUChild;
+
+using MappingPromise =
+ MozPromise<BufferMapResult, ipc::ResponseRejectReason, true>;
+
+class Device final : public DOMEventTargetHelper, public SupportsWeakPtr {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Device, DOMEventTargetHelper)
+ GPU_DECL_JS_WRAP(Device)
+
+ const RawId mId;
+ RefPtr<SupportedFeatures> mFeatures;
+ RefPtr<SupportedLimits> mLimits;
+
+ explicit Device(Adapter* const aParent, RawId aId,
+ UniquePtr<ffi::WGPULimits> aRawLimits);
+
+ RefPtr<WebGPUChild> GetBridge();
+ already_AddRefed<Texture> InitSwapChain(
+ const dom::GPUCanvasConfiguration& aDesc,
+ const layers::RemoteTextureOwnerId aOwnerId, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aDefaultSize);
+ bool CheckNewWarning(const nsACString& aMessage);
+
+ void CleanupUnregisteredInParent();
+
+ void GenerateError(const nsCString& aMessage);
+
+ bool IsLost() const;
+
+ private:
+ ~Device();
+ void Cleanup();
+
+ RefPtr<WebGPUChild> mBridge;
+ bool mValid = true;
+ nsString mLabel;
+ RefPtr<dom::Promise> mLostPromise;
+ RefPtr<Queue> mQueue;
+ nsTHashSet<nsCString> mKnownWarnings;
+
+ public:
+ void GetLabel(nsAString& aValue) const;
+ void SetLabel(const nsAString& aLabel);
+ dom::Promise* GetLost(ErrorResult& aRv);
+ dom::Promise* MaybeGetLost() const { return mLostPromise; }
+
+ const RefPtr<SupportedFeatures>& Features() const { return mFeatures; }
+ const RefPtr<SupportedLimits>& Limits() const { return mLimits; }
+ const RefPtr<Queue>& GetQueue() const { return mQueue; }
+
+ already_AddRefed<Buffer> CreateBuffer(const dom::GPUBufferDescriptor& aDesc,
+ ErrorResult& aRv);
+
+ already_AddRefed<Texture> CreateTexture(
+ const dom::GPUTextureDescriptor& aDesc);
+ already_AddRefed<Sampler> CreateSampler(
+ const dom::GPUSamplerDescriptor& aDesc);
+
+ already_AddRefed<CommandEncoder> CreateCommandEncoder(
+ const dom::GPUCommandEncoderDescriptor& aDesc);
+ already_AddRefed<RenderBundleEncoder> CreateRenderBundleEncoder(
+ const dom::GPURenderBundleEncoderDescriptor& aDesc);
+
+ already_AddRefed<BindGroupLayout> CreateBindGroupLayout(
+ const dom::GPUBindGroupLayoutDescriptor& aDesc);
+ already_AddRefed<PipelineLayout> CreatePipelineLayout(
+ const dom::GPUPipelineLayoutDescriptor& aDesc);
+ already_AddRefed<BindGroup> CreateBindGroup(
+ const dom::GPUBindGroupDescriptor& aDesc);
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<ShaderModule> CreateShaderModule(
+ JSContext* aCx, const dom::GPUShaderModuleDescriptor& aDesc);
+ already_AddRefed<ComputePipeline> CreateComputePipeline(
+ const dom::GPUComputePipelineDescriptor& aDesc);
+ already_AddRefed<RenderPipeline> CreateRenderPipeline(
+ const dom::GPURenderPipelineDescriptor& aDesc);
+ already_AddRefed<dom::Promise> CreateComputePipelineAsync(
+ const dom::GPUComputePipelineDescriptor& aDesc, ErrorResult& aRv);
+ already_AddRefed<dom::Promise> CreateRenderPipelineAsync(
+ const dom::GPURenderPipelineDescriptor& aDesc, ErrorResult& aRv);
+
+ void PushErrorScope(const dom::GPUErrorFilter& aFilter);
+ already_AddRefed<dom::Promise> PopErrorScope(ErrorResult& aRv);
+
+ void Destroy();
+
+ IMPL_EVENT_HANDLER(uncapturederror)
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_DEVICE_H_
diff --git a/dom/webgpu/DeviceLostInfo.cpp b/dom/webgpu/DeviceLostInfo.cpp
new file mode 100644
index 0000000000..4f1153ea60
--- /dev/null
+++ b/dom/webgpu/DeviceLostInfo.cpp
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeviceLostInfo.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(DeviceLostInfo, mGlobal)
+GPU_IMPL_JS_WRAP(DeviceLostInfo)
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/DeviceLostInfo.h b/dom/webgpu/DeviceLostInfo.h
new file mode 100644
index 0000000000..1ab77610c7
--- /dev/null
+++ b/dom/webgpu/DeviceLostInfo.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_DeviceLostInfo_H_
+#define GPU_DeviceLostInfo_H_
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/Maybe.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+class Device;
+
+class DeviceLostInfo final : public nsWrapperCache {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(DeviceLostInfo)
+ GPU_DECL_JS_WRAP(DeviceLostInfo)
+
+ explicit DeviceLostInfo(nsIGlobalObject* const aGlobal,
+ const nsAString& aMessage)
+ : mGlobal(aGlobal), mMessage(aMessage) {}
+ DeviceLostInfo(nsIGlobalObject* const aGlobal,
+ dom::GPUDeviceLostReason aReason, const nsAString& aMessage)
+ : mGlobal(aGlobal), mReason(Some(aReason)), mMessage(aMessage) {}
+
+ private:
+ ~DeviceLostInfo() = default;
+ void Cleanup() {}
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ const Maybe<dom::GPUDeviceLostReason> mReason;
+ const nsAutoString mMessage;
+
+ public:
+ void GetReason(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ if (!mReason || !dom::ToJSValue(aCx, mReason.value(), aRetval)) {
+ aRetval.setUndefined();
+ }
+ }
+
+ void GetMessage(nsAString& aValue) const { aValue = mMessage; }
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_DeviceLostInfo_H_
diff --git a/dom/webgpu/Instance.cpp b/dom/webgpu/Instance.cpp
new file mode 100644
index 0000000000..1eda84b415
--- /dev/null
+++ b/dom/webgpu/Instance.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Instance.h"
+
+#include "Adapter.h"
+#include "nsIGlobalObject.h"
+#include "ipc/WebGPUChild.h"
+#include "ipc/WebGPUTypes.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/CanvasManagerChild.h"
+#include "mozilla/gfx/gfxVars.h"
+
+#include <optional>
+#include <string_view>
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(Instance, mOwner)
+
+static inline nsDependentCString ToCString(const std::string_view s) {
+ return {s.data(), s.length()};
+}
+
+/*static*/
+already_AddRefed<Instance> Instance::Create(nsIGlobalObject* aOwner) {
+ RefPtr<Instance> result = new Instance(aOwner);
+ return result.forget();
+}
+
+Instance::Instance(nsIGlobalObject* aOwner) : mOwner(aOwner) {}
+
+Instance::~Instance() { Cleanup(); }
+
+void Instance::Cleanup() {}
+
+JSObject* Instance::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> givenProto) {
+ return dom::GPU_Binding::Wrap(cx, this, givenProto);
+}
+
+already_AddRefed<dom::Promise> Instance::RequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions, ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(mOwner, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // -
+ // Check if we should allow the request.
+
+ const auto errStr = [&]() -> std::optional<std::string_view> {
+#ifdef RELEASE_OR_BETA
+ if (true) {
+ return "WebGPU is not yet available in Release or Beta builds.";
+ }
+#endif
+ if (!gfx::gfxVars::AllowWebGPU()) {
+ return "WebGPU is disabled by blocklist.";
+ }
+ if (!StaticPrefs::dom_webgpu_enabled()) {
+ return "WebGPU is disabled by dom.webgpu.enabled:false.";
+ }
+ return {};
+ }();
+ if (errStr) {
+ promise->MaybeRejectWithNotSupportedError(ToCString(*errStr));
+ return promise.forget();
+ }
+
+ // -
+ // Make the request.
+
+ auto* const canvasManager = gfx::CanvasManagerChild::Get();
+ if (!canvasManager) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Failed to create CanavasManagerChild");
+ return promise.forget();
+ }
+
+ RefPtr<WebGPUChild> bridge = canvasManager->GetWebGPUChild();
+ if (!bridge) {
+ promise->MaybeRejectWithInvalidStateError("Failed to create WebGPUChild");
+ return promise.forget();
+ }
+
+ RefPtr<Instance> instance = this;
+
+ bridge->InstanceRequestAdapter(aOptions)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, instance, bridge](ipc::ByteBuf aInfoBuf) {
+ auto info = std::make_shared<ffi::WGPUAdapterInformation>();
+ ffi::wgpu_client_adapter_extract_info(ToFFI(&aInfoBuf), info.get());
+ MOZ_ASSERT(info->id != 0);
+ RefPtr<Adapter> adapter = new Adapter(instance, bridge, info);
+ promise->MaybeResolve(adapter);
+ },
+ [promise](const Maybe<ipc::ResponseRejectReason>& aResponseReason) {
+ if (aResponseReason.isSome()) {
+ promise->MaybeRejectWithAbortError("Internal communication error!");
+ } else {
+ promise->MaybeResolve(JS::NullHandleValue);
+ }
+ });
+
+ return promise.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Instance.h b/dom/webgpu/Instance.h
new file mode 100644
index 0000000000..7849c34e64
--- /dev/null
+++ b/dom/webgpu/Instance.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_INSTANCE_H_
+#define GPU_INSTANCE_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/layers/BuildConstants.h"
+#include "nsCOMPtr.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+class Promise;
+struct GPURequestAdapterOptions;
+} // namespace dom
+
+namespace webgpu {
+class Adapter;
+class GPUAdapter;
+class WebGPUChild;
+
+class Instance final : public nsWrapperCache {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(Instance)
+ GPU_DECL_JS_WRAP(Instance)
+
+ nsIGlobalObject* GetParentObject() const { return mOwner; }
+
+ static already_AddRefed<Instance> Create(nsIGlobalObject* aOwner);
+
+ already_AddRefed<dom::Promise> RequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions, ErrorResult& aRv);
+
+ dom::GPUTextureFormat GetPreferredCanvasFormat() const {
+ if (kIsAndroid) {
+ return dom::GPUTextureFormat::Rgba8unorm;
+ }
+ return dom::GPUTextureFormat::Bgra8unorm;
+ };
+
+ private:
+ explicit Instance(nsIGlobalObject* aOwner);
+ virtual ~Instance();
+ void Cleanup();
+
+ nsCOMPtr<nsIGlobalObject> mOwner;
+
+ public:
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_INSTANCE_H_
diff --git a/dom/webgpu/ObjectModel.cpp b/dom/webgpu/ObjectModel.cpp
new file mode 100644
index 0000000000..f691a17b66
--- /dev/null
+++ b/dom/webgpu/ObjectModel.cpp
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ObjectModel.h"
+
+#include "Adapter.h"
+#include "ShaderModule.h"
+#include "CompilationInfo.h"
+#include "Device.h"
+#include "CommandEncoder.h"
+#include "Instance.h"
+#include "Texture.h"
+
+namespace mozilla::webgpu {
+
+template <typename T>
+ChildOf<T>::ChildOf(T* const parent) : mParent(parent) {}
+
+template <typename T>
+ChildOf<T>::~ChildOf() = default;
+
+template <typename T>
+nsIGlobalObject* ChildOf<T>::GetParentObject() const {
+ return mParent->GetParentObject();
+}
+
+void ObjectBase::GetLabel(nsAString& aValue) const { aValue = mLabel; }
+void ObjectBase::SetLabel(const nsAString& aLabel) { mLabel = aLabel; }
+
+template class ChildOf<Adapter>;
+template class ChildOf<ShaderModule>;
+template class ChildOf<CompilationInfo>;
+template class ChildOf<CommandEncoder>;
+template class ChildOf<Device>;
+template class ChildOf<Instance>;
+template class ChildOf<Texture>;
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ObjectModel.h b/dom/webgpu/ObjectModel.h
new file mode 100644
index 0000000000..d482ac16f5
--- /dev/null
+++ b/dom/webgpu/ObjectModel.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_OBJECT_MODEL_H_
+#define GPU_OBJECT_MODEL_H_
+
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::webgpu {
+class WebGPUChild;
+
+template <typename T>
+class ChildOf {
+ protected:
+ explicit ChildOf(T* const parent);
+ virtual ~ChildOf();
+
+ RefPtr<T> mParent;
+
+ public:
+ nsIGlobalObject* GetParentObject() const;
+};
+
+class ObjectBase : public nsWrapperCache {
+ private:
+ nsString mLabel;
+
+ protected:
+ virtual ~ObjectBase() = default;
+
+ // False if this object is definitely invalid.
+ //
+ // See WebGPU §3.2, "Invalid Internal Objects & Contagious Invalidity".
+ //
+ // There could also be state in the GPU process indicating that our
+ // counterpart object there is invalid; certain GPU process operations will
+ // report an error back to use if we try to use it. But if it's useful to know
+ // whether the object is "definitely invalid", this should suffice.
+ bool mValid = true;
+
+ public:
+ // Return true if this WebGPU object may be valid.
+ //
+ // This is used by methods that want to know whether somebody other than
+ // `this` is valid. Generally, WebGPU object methods check `this->mValid`
+ // directly.
+ bool IsValid() const { return mValid; }
+
+ void GetLabel(nsAString& aValue) const;
+ void SetLabel(const nsAString& aLabel);
+};
+
+} // namespace mozilla::webgpu
+
+#define GPU_DECL_JS_WRAP(T) \
+ JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> givenProto) \
+ override;
+
+#define GPU_DECL_CYCLE_COLLECTION(T) \
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(T) \
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(T)
+
+#define GPU_IMPL_JS_WRAP(T) \
+ JSObject* T::WrapObject(JSContext* cx, JS::Handle<JSObject*> givenProto) { \
+ return dom::GPU##T##_Binding::Wrap(cx, this, givenProto); \
+ }
+
+// Note: we don't use `NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE` directly
+// because there is a custom action we need to always do.
+#define GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(T, ...) \
+ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(T) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(T) \
+ tmp->Cleanup(); \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_END \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(T) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+#define GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK_PTR(T, ...) \
+ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(T) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(T) \
+ tmp->Cleanup(); \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_END \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(T) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+#define GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(T, P, ...) \
+ NS_IMPL_CYCLE_COLLECTION_CLASS(T) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(T, P) \
+ tmp->Cleanup(); \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR \
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_END \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(T, P) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(__VA_ARGS__) \
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+#define GPU_IMPL_CYCLE_COLLECTION(T, ...) \
+ GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(T, __VA_ARGS__)
+
+template <typename T>
+void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& callback,
+ nsTArray<RefPtr<const T>>& field,
+ const char* name, uint32_t flags) {
+ for (auto& element : field) {
+ CycleCollectionNoteChild(callback, const_cast<T*>(element.get()), name,
+ flags);
+ }
+}
+
+template <typename T>
+void ImplCycleCollectionUnlink(nsTArray<RefPtr<const T>>& field) {
+ for (auto& element : field) {
+ ImplCycleCollectionUnlink(element);
+ }
+ field.Clear();
+}
+
+#endif // GPU_OBJECT_MODEL_H_
diff --git a/dom/webgpu/OutOfMemoryError.cpp b/dom/webgpu/OutOfMemoryError.cpp
new file mode 100644
index 0000000000..edf52369df
--- /dev/null
+++ b/dom/webgpu/OutOfMemoryError.cpp
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OutOfMemoryError.h"
+#include "Device.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(OutOfMemoryError, mParent)
+GPU_IMPL_JS_WRAP(OutOfMemoryError)
+
+OutOfMemoryError::~OutOfMemoryError() = default;
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/OutOfMemoryError.h b/dom/webgpu/OutOfMemoryError.h
new file mode 100644
index 0000000000..e634e396f1
--- /dev/null
+++ b/dom/webgpu/OutOfMemoryError.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_OutOfMemoryError_H_
+#define GPU_OutOfMemoryError_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+namespace dom {
+class GlobalObject;
+} // namespace dom
+namespace webgpu {
+class Device;
+
+class OutOfMemoryError final : public nsWrapperCache, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(OutOfMemoryError)
+ GPU_DECL_JS_WRAP(OutOfMemoryError)
+ OutOfMemoryError() = delete;
+
+ private:
+ virtual ~OutOfMemoryError();
+ void Cleanup() {}
+
+ public:
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_OutOfMemoryError_H_
diff --git a/dom/webgpu/PipelineLayout.cpp b/dom/webgpu/PipelineLayout.cpp
new file mode 100644
index 0000000000..21485e78a5
--- /dev/null
+++ b/dom/webgpu/PipelineLayout.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "PipelineLayout.h"
+#include "ipc/WebGPUChild.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(PipelineLayout, mParent)
+GPU_IMPL_JS_WRAP(PipelineLayout)
+
+PipelineLayout::PipelineLayout(Device* const aParent, RawId aId)
+ : ChildOf(aParent), mId(aId) {
+ if (!aId) {
+ mValid = false;
+ }
+}
+
+PipelineLayout::~PipelineLayout() { Cleanup(); }
+
+void PipelineLayout::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendPipelineLayoutDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/PipelineLayout.h b/dom/webgpu/PipelineLayout.h
new file mode 100644
index 0000000000..65293d778d
--- /dev/null
+++ b/dom/webgpu/PipelineLayout.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_PipelineLayout_H_
+#define GPU_PipelineLayout_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class PipelineLayout final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(PipelineLayout)
+ GPU_DECL_JS_WRAP(PipelineLayout)
+
+ PipelineLayout(Device* const aParent, RawId aId);
+
+ const RawId mId;
+
+ private:
+ virtual ~PipelineLayout();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_PipelineLayout_H_
diff --git a/dom/webgpu/QuerySet.cpp b/dom/webgpu/QuerySet.cpp
new file mode 100644
index 0000000000..05f30f6cc8
--- /dev/null
+++ b/dom/webgpu/QuerySet.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "QuerySet.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+QuerySet::~QuerySet() = default;
+
+GPU_IMPL_CYCLE_COLLECTION(QuerySet, mParent)
+GPU_IMPL_JS_WRAP(QuerySet)
+
+void QuerySet::Destroy() {
+ // TODO
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/QuerySet.h b/dom/webgpu/QuerySet.h
new file mode 100644
index 0000000000..e7e6f4968b
--- /dev/null
+++ b/dom/webgpu/QuerySet.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_QuerySet_H_
+#define GPU_QuerySet_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class QuerySet final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(QuerySet)
+ GPU_DECL_JS_WRAP(QuerySet)
+
+ QuerySet() = delete;
+ void Destroy();
+
+ private:
+ virtual ~QuerySet();
+ void Cleanup() {}
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_QuerySet_H_
diff --git a/dom/webgpu/Queue.cpp b/dom/webgpu/Queue.cpp
new file mode 100644
index 0000000000..c675c42cae
--- /dev/null
+++ b/dom/webgpu/Queue.cpp
@@ -0,0 +1,475 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "Queue.h"
+
+#include <algorithm>
+
+#include "CommandBuffer.h"
+#include "CommandEncoder.h"
+#include "ipc/WebGPUChild.h"
+#include "mozilla/Casting.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/ImageBitmap.h"
+#include "mozilla/dom/OffscreenCanvas.h"
+#include "mozilla/dom/WebGLTexelConversions.h"
+#include "mozilla/dom/WebGLTypes.h"
+#include "nsLayoutUtils.h"
+#include "Utility.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(Queue, mParent, mBridge)
+GPU_IMPL_JS_WRAP(Queue)
+
+Queue::Queue(Device* const aParent, WebGPUChild* aBridge, RawId aId)
+ : ChildOf(aParent), mBridge(aBridge), mId(aId) {}
+
+Queue::~Queue() { Cleanup(); }
+
+void Queue::Submit(
+ const dom::Sequence<OwningNonNull<CommandBuffer>>& aCommandBuffers) {
+ nsTArray<RawId> list(aCommandBuffers.Length());
+ for (uint32_t i = 0; i < aCommandBuffers.Length(); ++i) {
+ auto idMaybe = aCommandBuffers[i]->Commit();
+ if (idMaybe) {
+ list.AppendElement(*idMaybe);
+ }
+ }
+
+ mBridge->SendQueueSubmit(mId, mParent->mId, list);
+}
+
+// Get the base address and length of part of a `BufferSource`.
+//
+// Given `aBufferSource` and an offset `aDataOffset` and optional length
+// `aSizeOrRemainder` describing the range of its contents we want to see, check
+// all arguments and set `aDataContents` and `aContentsSize` to a pointer to the
+// bytes and a length. Report errors in `aRv`.
+//
+// If `ASizeOrRemainder` was not passed, return a view from the starting offset
+// to the end of `aBufferSource`.
+//
+// On success, the returned `aDataContents` is never `nullptr`. If the
+// `ArrayBuffer` is detached, return a pointer to a dummy buffer and set
+// `aContentsSize` to zero.
+//
+// The `aBufferSource` argument is a WebIDL `BufferSource`, which WebGPU methods
+// use anywhere they accept a block of raw bytes. WebIDL defines `BufferSource`
+// as:
+//
+// typedef (ArrayBufferView or ArrayBuffer) BufferSource;
+//
+// This appears in Gecko code as `dom::ArrayBufferViewOrArrayBuffer`.
+static void GetBufferSourceDataAndSize(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferSource,
+ uint64_t aDataOffset, const dom::Optional<uint64_t>& aSizeOrRemainder,
+ uint8_t*& aDataContents, uint64_t& aContentsSize, const char* aOffsetName,
+ ErrorResult& aRv) {
+ uint64_t dataSize = 0;
+ uint8_t* dataContents = nullptr;
+ if (aBufferSource.IsArrayBufferView()) {
+ const auto& view = aBufferSource.GetAsArrayBufferView();
+ view.ComputeState();
+ dataSize = view.Length();
+ dataContents = view.Data();
+ }
+ if (aBufferSource.IsArrayBuffer()) {
+ const auto& ab = aBufferSource.GetAsArrayBuffer();
+ ab.ComputeState();
+ dataSize = ab.Length();
+ dataContents = ab.Data();
+ }
+
+ if (aDataOffset > dataSize) {
+ aRv.ThrowOperationError(
+ nsPrintfCString("%s is greater than data length", aOffsetName));
+ return;
+ }
+
+ uint64_t contentsSize = 0;
+ if (aSizeOrRemainder.WasPassed()) {
+ contentsSize = aSizeOrRemainder.Value();
+ } else {
+ // We already know that aDataOffset <= length, so this cannot underflow.
+ contentsSize = dataSize - aDataOffset;
+ }
+
+ // This could be folded into the if above, but it's nice to make it
+ // obvious that the check always occurs.
+ // We already know that aDataOffset <= length, so this cannot underflow.
+ if (contentsSize > dataSize - aDataOffset) {
+ aRv.ThrowOperationError(
+ nsPrintfCString("%s + size is greater than data length", aOffsetName));
+ return;
+ }
+
+ if (!dataContents) {
+ // Passing `nullptr` as either the source or destination to
+ // `memcpy` is undefined behavior, even when the count is zero:
+ //
+ // https://en.cppreference.com/w/cpp/string/byte/memcpy
+ //
+ // We can either make callers responsible for checking the pointer
+ // before calling `memcpy`, or we can have it point to a
+ // permanently-live `static` dummy byte, so that the copies are
+ // harmless. The latter seems less error-prone.
+ static uint8_t dummy;
+ dataContents = &dummy;
+ MOZ_RELEASE_ASSERT(contentsSize == 0);
+ }
+ aDataContents = dataContents;
+ aContentsSize = contentsSize;
+}
+
+void Queue::WriteBuffer(const Buffer& aBuffer, uint64_t aBufferOffset,
+ const dom::ArrayBufferViewOrArrayBuffer& aData,
+ uint64_t aDataOffset,
+ const dom::Optional<uint64_t>& aSize,
+ ErrorResult& aRv) {
+ uint8_t* dataContents = nullptr;
+ uint64_t contentsSize = 0;
+ GetBufferSourceDataAndSize(aData, aDataOffset, aSize, dataContents,
+ contentsSize, "dataOffset", aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (contentsSize % 4 != 0) {
+ aRv.ThrowAbortError("Byte size must be a multiple of 4");
+ return;
+ }
+
+ auto alloc =
+ mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap(contentsSize);
+ if (alloc.isNothing()) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto handle = std::move(alloc.ref().first);
+ auto mapping = std::move(alloc.ref().second);
+
+ memcpy(mapping.Bytes().data(), dataContents + aDataOffset, contentsSize);
+ ipc::ByteBuf bb;
+ ffi::wgpu_queue_write_buffer(aBuffer.mId, aBufferOffset, ToFFI(&bb));
+ if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb),
+ std::move(handle))) {
+ MOZ_CRASH("IPC failure");
+ }
+}
+
+void Queue::WriteTexture(const dom::GPUImageCopyTexture& aDestination,
+ const dom::ArrayBufferViewOrArrayBuffer& aData,
+ const dom::GPUImageDataLayout& aDataLayout,
+ const dom::GPUExtent3D& aSize, ErrorResult& aRv) {
+ ffi::WGPUImageCopyTexture copyView = {};
+ CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, &copyView);
+ ffi::WGPUImageDataLayout dataLayout = {};
+ CommandEncoder::ConvertTextureDataLayoutToFFI(aDataLayout, &dataLayout);
+ dataLayout.offset = 0; // our Shmem has the contents starting from 0.
+ ffi::WGPUExtent3d extent = {};
+ ConvertExtent3DToFFI(aSize, &extent);
+
+ uint8_t* dataContents = nullptr;
+ uint64_t contentsSize = 0;
+ GetBufferSourceDataAndSize(aData, aDataLayout.mOffset,
+ dom::Optional<uint64_t>(), dataContents,
+ contentsSize, "dataLayout.offset", aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (!contentsSize) {
+ aRv.ThrowAbortError("Input size cannot be zero.");
+ return;
+ }
+ MOZ_ASSERT(dataContents != nullptr);
+
+ auto alloc =
+ mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap(contentsSize);
+ if (alloc.isNothing()) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto handle = std::move(alloc.ref().first);
+ auto mapping = std::move(alloc.ref().second);
+
+ memcpy(mapping.Bytes().data(), dataContents + aDataLayout.mOffset,
+ contentsSize);
+
+ ipc::ByteBuf bb;
+ ffi::wgpu_queue_write_texture(copyView, dataLayout, extent, ToFFI(&bb));
+ if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb),
+ std::move(handle))) {
+ MOZ_CRASH("IPC failure");
+ }
+}
+
+static WebGLTexelFormat ToWebGLTexelFormat(gfx::SurfaceFormat aFormat) {
+ switch (aFormat) {
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ return WebGLTexelFormat::BGRA8;
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::R8G8B8X8:
+ return WebGLTexelFormat::RGBA8;
+ default:
+ return WebGLTexelFormat::FormatNotSupportingAnyConversion;
+ }
+}
+
+static WebGLTexelFormat ToWebGLTexelFormat(dom::GPUTextureFormat aFormat) {
+ // TODO: We need support for Rbg10a2unorm as well.
+ switch (aFormat) {
+ case dom::GPUTextureFormat::R8unorm:
+ return WebGLTexelFormat::R8;
+ case dom::GPUTextureFormat::R16float:
+ return WebGLTexelFormat::R16F;
+ case dom::GPUTextureFormat::R32float:
+ return WebGLTexelFormat::R32F;
+ case dom::GPUTextureFormat::Rg8unorm:
+ return WebGLTexelFormat::RG8;
+ case dom::GPUTextureFormat::Rg16float:
+ return WebGLTexelFormat::RG16F;
+ case dom::GPUTextureFormat::Rg32float:
+ return WebGLTexelFormat::RG32F;
+ case dom::GPUTextureFormat::Rgba8unorm:
+ case dom::GPUTextureFormat::Rgba8unorm_srgb:
+ return WebGLTexelFormat::RGBA8;
+ case dom::GPUTextureFormat::Bgra8unorm:
+ case dom::GPUTextureFormat::Bgra8unorm_srgb:
+ return WebGLTexelFormat::BGRA8;
+ case dom::GPUTextureFormat::Rgba16float:
+ return WebGLTexelFormat::RGBA16F;
+ case dom::GPUTextureFormat::Rgba32float:
+ return WebGLTexelFormat::RGBA32F;
+ default:
+ return WebGLTexelFormat::FormatNotSupportingAnyConversion;
+ }
+}
+
+void Queue::CopyExternalImageToTexture(
+ const dom::GPUImageCopyExternalImage& aSource,
+ const dom::GPUImageCopyTextureTagged& aDestination,
+ const dom::GPUExtent3D& aCopySize, ErrorResult& aRv) {
+ const auto dstFormat = ToWebGLTexelFormat(aDestination.mTexture->Format());
+ if (dstFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) {
+ aRv.ThrowInvalidStateError("Unsupported destination format");
+ return;
+ }
+
+ const uint32_t surfaceFlags = nsLayoutUtils::SFE_ALLOW_NON_PREMULT;
+ SurfaceFromElementResult sfeResult;
+ switch (aSource.mSource.GetType()) {
+ case decltype(aSource.mSource)::Type::eImageBitmap: {
+ const auto& bitmap = aSource.mSource.GetAsImageBitmap();
+ if (bitmap->IsClosed()) {
+ aRv.ThrowInvalidStateError("Detached ImageBitmap");
+ return;
+ }
+
+ sfeResult = nsLayoutUtils::SurfaceFromImageBitmap(bitmap, surfaceFlags);
+ break;
+ }
+ case decltype(aSource.mSource)::Type::eHTMLCanvasElement: {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const auto& canvas = aSource.mSource.GetAsHTMLCanvasElement();
+ if (canvas->Width() == 0 || canvas->Height() == 0) {
+ aRv.ThrowInvalidStateError("Zero-sized HTMLCanvasElement");
+ return;
+ }
+
+ sfeResult = nsLayoutUtils::SurfaceFromElement(canvas, surfaceFlags);
+ break;
+ }
+ case decltype(aSource.mSource)::Type::eOffscreenCanvas: {
+ const auto& canvas = aSource.mSource.GetAsOffscreenCanvas();
+ if (canvas->Width() == 0 || canvas->Height() == 0) {
+ aRv.ThrowInvalidStateError("Zero-sized OffscreenCanvas");
+ return;
+ }
+
+ sfeResult =
+ nsLayoutUtils::SurfaceFromOffscreenCanvas(canvas, surfaceFlags);
+ break;
+ }
+ }
+
+ if (!sfeResult.mCORSUsed) {
+ nsIGlobalObject* global = mParent->GetOwnerGlobal();
+ nsIPrincipal* dstPrincipal = global ? global->PrincipalOrNull() : nullptr;
+ if (!sfeResult.mPrincipal || !dstPrincipal ||
+ !dstPrincipal->Subsumes(sfeResult.mPrincipal)) {
+ aRv.ThrowSecurityError("Cross-origin elements require CORS!");
+ return;
+ }
+ }
+
+ if (sfeResult.mIsWriteOnly) {
+ aRv.ThrowSecurityError("Write only source data not supported!");
+ return;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = sfeResult.GetSourceSurface();
+ if (!surface) {
+ aRv.ThrowInvalidStateError("No surface available from source");
+ return;
+ }
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
+ if (!dataSurface) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ bool srcPremultiplied;
+ switch (sfeResult.mAlphaType) {
+ case gfxAlphaType::Premult:
+ srcPremultiplied = true;
+ break;
+ case gfxAlphaType::NonPremult:
+ srcPremultiplied = false;
+ break;
+ case gfxAlphaType::Opaque:
+ // No (un)premultiplication necessary so match the output.
+ srcPremultiplied = aDestination.mPremultipliedAlpha;
+ break;
+ }
+
+ const auto surfaceFormat = dataSurface->GetFormat();
+ const auto srcFormat = ToWebGLTexelFormat(surfaceFormat);
+ if (srcFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) {
+ gfxCriticalError() << "Unsupported surface format from source "
+ << surfaceFormat;
+ MOZ_CRASH();
+ }
+
+ gfx::DataSourceSurface::ScopedMap map(dataSurface,
+ gfx::DataSourceSurface::READ);
+ if (!map.IsMapped()) {
+ aRv.ThrowInvalidStateError("Cannot map surface from source");
+ return;
+ }
+
+ if (!aSource.mOrigin.IsGPUOrigin2DDict()) {
+ aRv.ThrowInvalidStateError("Cannot get origin from source");
+ return;
+ }
+
+ ffi::WGPUExtent3d extent = {};
+ ConvertExtent3DToFFI(aCopySize, &extent);
+ if (extent.depth_or_array_layers > 1) {
+ aRv.ThrowOperationError("Depth is greater than 1");
+ return;
+ }
+
+ uint32_t srcOriginX;
+ uint32_t srcOriginY;
+ if (aSource.mOrigin.IsRangeEnforcedUnsignedLongSequence()) {
+ const auto& seq = aSource.mOrigin.GetAsRangeEnforcedUnsignedLongSequence();
+ srcOriginX = seq.Length() > 0 ? seq[0] : 0;
+ srcOriginY = seq.Length() > 1 ? seq[1] : 0;
+ } else if (aSource.mOrigin.IsGPUOrigin2DDict()) {
+ const auto& dict = aSource.mOrigin.GetAsGPUOrigin2DDict();
+ srcOriginX = dict.mX;
+ srcOriginY = dict.mY;
+ } else {
+ MOZ_CRASH("Unexpected origin type!");
+ }
+
+ const auto checkedMaxWidth = CheckedInt<uint32_t>(srcOriginX) + extent.width;
+ const auto checkedMaxHeight =
+ CheckedInt<uint32_t>(srcOriginY) + extent.height;
+ if (!checkedMaxWidth.isValid() || !checkedMaxHeight.isValid()) {
+ aRv.ThrowOperationError("Offset and copy size exceed integer bounds");
+ return;
+ }
+
+ const gfx::IntSize surfaceSize = dataSurface->GetSize();
+ const auto surfaceWidth = AssertedCast<uint32_t>(surfaceSize.width);
+ const auto surfaceHeight = AssertedCast<uint32_t>(surfaceSize.height);
+ if (surfaceWidth < checkedMaxWidth.value() ||
+ surfaceHeight < checkedMaxHeight.value()) {
+ aRv.ThrowOperationError("Offset and copy size exceed surface bounds");
+ return;
+ }
+
+ const auto dstWidth = extent.width;
+ const auto dstHeight = extent.height;
+ if (dstWidth == 0 || dstHeight == 0) {
+ aRv.ThrowOperationError("Destination size is empty");
+ return;
+ }
+
+ if (!aDestination.mTexture->mBytesPerBlock) {
+ // TODO(bug 1781071) This should emmit a GPUValidationError on the device
+ // timeline.
+ aRv.ThrowInvalidStateError("Invalid destination format");
+ return;
+ }
+
+ // Note: This assumes bytes per block == bytes per pixel which is the case
+ // here because the spec only allows non-compressed texture formats for the
+ // destination.
+ const auto dstStride = CheckedInt<uint32_t>(extent.width) *
+ aDestination.mTexture->mBytesPerBlock.value();
+ const auto dstByteLength = dstStride * extent.height;
+ if (!dstStride.isValid() || !dstByteLength.isValid()) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto alloc = mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap(
+ dstByteLength.value());
+ if (alloc.isNothing()) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto handle = std::move(alloc.ref().first);
+ auto mapping = std::move(alloc.ref().second);
+
+ const int32_t pixelSize = gfx::BytesPerPixel(surfaceFormat);
+ auto* dstBegin = mapping.Bytes().data();
+ const auto* srcBegin =
+ map.GetData() + srcOriginX * pixelSize + srcOriginY * map.GetStride();
+ const auto srcOriginPos = gl::OriginPos::TopLeft;
+ const auto srcStride = AssertedCast<uint32_t>(map.GetStride());
+ const auto dstOriginPos =
+ aSource.mFlipY ? gl::OriginPos::BottomLeft : gl::OriginPos::TopLeft;
+ bool wasTrivial;
+
+ auto dstStrideVal = dstStride.value();
+
+ if (!ConvertImage(dstWidth, dstHeight, srcBegin, srcStride, srcOriginPos,
+ srcFormat, srcPremultiplied, dstBegin, dstStrideVal,
+ dstOriginPos, dstFormat, aDestination.mPremultipliedAlpha,
+ &wasTrivial)) {
+ MOZ_ASSERT_UNREACHABLE("ConvertImage failed!");
+ aRv.ThrowInvalidStateError(
+ nsPrintfCString("Failed to convert source to destination format "
+ "(%i/%i), please file a bug!",
+ (int)srcFormat, (int)dstFormat));
+ return;
+ }
+
+ ffi::WGPUImageDataLayout dataLayout = {0, &dstStrideVal, &dstHeight};
+ ffi::WGPUImageCopyTexture copyView = {};
+ CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, &copyView);
+ ipc::ByteBuf bb;
+ ffi::wgpu_queue_write_texture(copyView, dataLayout, extent, ToFFI(&bb));
+ if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb),
+ std::move(handle))) {
+ MOZ_CRASH("IPC failure");
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Queue.h b/dom/webgpu/Queue.h
new file mode 100644
index 0000000000..27fb23c780
--- /dev/null
+++ b/dom/webgpu/Queue.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_Queue_H_
+#define GPU_Queue_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+class RangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+class ArrayBufferViewOrArrayBuffer;
+template <typename T>
+class Optional;
+template <typename T>
+class Sequence;
+struct GPUImageCopyTexture;
+struct GPUImageDataLayout;
+struct TextureCopyView;
+struct TextureDataLayout;
+using GPUExtent3D = RangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+} // namespace dom
+namespace webgpu {
+
+class Buffer;
+class CommandBuffer;
+class Device;
+class Fence;
+
+class Queue final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(Queue)
+ GPU_DECL_JS_WRAP(Queue)
+
+ Queue(Device* const aParent, WebGPUChild* aBridge, RawId aId);
+
+ void Submit(
+ const dom::Sequence<OwningNonNull<CommandBuffer>>& aCommandBuffers);
+
+ void WriteBuffer(const Buffer& aBuffer, uint64_t aBufferOffset,
+ const dom::ArrayBufferViewOrArrayBuffer& aData,
+ uint64_t aDataOffset, const dom::Optional<uint64_t>& aSize,
+ ErrorResult& aRv);
+
+ void WriteTexture(const dom::GPUImageCopyTexture& aDestination,
+ const dom::ArrayBufferViewOrArrayBuffer& aData,
+ const dom::GPUImageDataLayout& aDataLayout,
+ const dom::GPUExtent3D& aSize, ErrorResult& aRv);
+
+ void CopyExternalImageToTexture(
+ const dom::GPUImageCopyExternalImage& aSource,
+ const dom::GPUImageCopyTextureTagged& aDestination,
+ const dom::GPUExtent3D& aCopySize, ErrorResult& aRv);
+
+ private:
+ virtual ~Queue();
+ void Cleanup() {}
+
+ RefPtr<WebGPUChild> mBridge;
+ const RawId mId;
+
+ public:
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_Queue_H_
diff --git a/dom/webgpu/RenderBundle.cpp b/dom/webgpu/RenderBundle.cpp
new file mode 100644
index 0000000000..0e6ce87874
--- /dev/null
+++ b/dom/webgpu/RenderBundle.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "RenderBundle.h"
+
+#include "Device.h"
+#include "ipc/WebGPUChild.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(RenderBundle, mParent)
+GPU_IMPL_JS_WRAP(RenderBundle)
+
+RenderBundle::RenderBundle(Device* const aParent, RawId aId)
+ : ChildOf(aParent), mId(aId) {
+ // If we happened to finish an encoder twice, the second
+ // bundle should be invalid.
+ if (!mId) {
+ mValid = false;
+ }
+}
+
+RenderBundle::~RenderBundle() { Cleanup(); }
+
+void RenderBundle::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendRenderBundleDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/RenderBundle.h b/dom/webgpu/RenderBundle.h
new file mode 100644
index 0000000000..0fef6af781
--- /dev/null
+++ b/dom/webgpu/RenderBundle.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_RenderBundle_H_
+#define GPU_RenderBundle_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class RenderBundle final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(RenderBundle)
+ GPU_DECL_JS_WRAP(RenderBundle)
+
+ RenderBundle(Device* const aParent, RawId aId);
+
+ const RawId mId;
+
+ private:
+ virtual ~RenderBundle();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_RenderBundle_H_
diff --git a/dom/webgpu/RenderBundleEncoder.cpp b/dom/webgpu/RenderBundleEncoder.cpp
new file mode 100644
index 0000000000..cb14db1dd0
--- /dev/null
+++ b/dom/webgpu/RenderBundleEncoder.cpp
@@ -0,0 +1,196 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "RenderBundleEncoder.h"
+
+#include "BindGroup.h"
+#include "Buffer.h"
+#include "RenderBundle.h"
+#include "RenderPipeline.h"
+#include "ipc/WebGPUChild.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(RenderBundleEncoder, mParent, mUsedBindGroups,
+ mUsedBuffers, mUsedPipelines, mUsedTextureViews)
+GPU_IMPL_JS_WRAP(RenderBundleEncoder)
+
+ffi::WGPURenderBundleEncoder* ScopedFfiBundleTraits::empty() { return nullptr; }
+
+void ScopedFfiBundleTraits::release(ffi::WGPURenderBundleEncoder* raw) {
+ if (raw) {
+ ffi::wgpu_render_bundle_encoder_destroy(raw);
+ }
+}
+
+ffi::WGPURenderBundleEncoder* CreateRenderBundleEncoder(
+ RawId aDeviceId, const dom::GPURenderBundleEncoderDescriptor& aDesc,
+ WebGPUChild* const aBridge) {
+ if (!aBridge->CanSend()) {
+ return nullptr;
+ }
+
+ ffi::WGPURenderBundleEncoderDescriptor desc = {};
+ desc.sample_count = aDesc.mSampleCount;
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ffi::WGPUTextureFormat depthStencilFormat = {ffi::WGPUTextureFormat_Sentinel};
+ if (aDesc.mDepthStencilFormat.WasPassed()) {
+ WebGPUChild::ConvertTextureFormatRef(aDesc.mDepthStencilFormat.Value(),
+ depthStencilFormat);
+ desc.depth_stencil_format = &depthStencilFormat;
+ }
+
+ std::vector<ffi::WGPUTextureFormat> colorFormats = {};
+ for (const auto i : IntegerRange(aDesc.mColorFormats.Length())) {
+ ffi::WGPUTextureFormat format = {ffi::WGPUTextureFormat_Sentinel};
+ WebGPUChild::ConvertTextureFormatRef(aDesc.mColorFormats[i], format);
+ colorFormats.push_back(format);
+ }
+
+ desc.color_formats = colorFormats.data();
+ desc.color_formats_length = colorFormats.size();
+
+ ipc::ByteBuf failureAction;
+ auto* bundle = ffi::wgpu_device_create_render_bundle_encoder(
+ aDeviceId, &desc, ToFFI(&failureAction));
+ // report an error only if the operation failed
+ if (!bundle &&
+ !aBridge->SendDeviceAction(aDeviceId, std::move(failureAction))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return bundle;
+}
+
+RenderBundleEncoder::RenderBundleEncoder(
+ Device* const aParent, WebGPUChild* const aBridge,
+ const dom::GPURenderBundleEncoderDescriptor& aDesc)
+ : ChildOf(aParent),
+ mEncoder(CreateRenderBundleEncoder(aParent->mId, aDesc, aBridge)) {
+ mValid = mEncoder.get() != nullptr;
+}
+
+RenderBundleEncoder::~RenderBundleEncoder() { Cleanup(); }
+
+void RenderBundleEncoder::Cleanup() {
+ if (mValid) {
+ mValid = false;
+ }
+}
+
+void RenderBundleEncoder::SetBindGroup(
+ uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets) {
+ if (mValid) {
+ mUsedBindGroups.AppendElement(&aBindGroup);
+ ffi::wgpu_render_bundle_set_bind_group(mEncoder, aSlot, aBindGroup.mId,
+ aDynamicOffsets.Elements(),
+ aDynamicOffsets.Length());
+ }
+}
+
+void RenderBundleEncoder::SetPipeline(const RenderPipeline& aPipeline) {
+ if (mValid) {
+ mUsedPipelines.AppendElement(&aPipeline);
+ ffi::wgpu_render_bundle_set_pipeline(mEncoder, aPipeline.mId);
+ }
+}
+
+void RenderBundleEncoder::SetIndexBuffer(
+ const Buffer& aBuffer, const dom::GPUIndexFormat& aIndexFormat,
+ uint64_t aOffset, uint64_t aSize) {
+ if (mValid) {
+ mUsedBuffers.AppendElement(&aBuffer);
+ const auto iformat = aIndexFormat == dom::GPUIndexFormat::Uint32
+ ? ffi::WGPUIndexFormat_Uint32
+ : ffi::WGPUIndexFormat_Uint16;
+ ffi::wgpu_render_bundle_set_index_buffer(mEncoder, aBuffer.mId, iformat,
+ aOffset, aSize);
+ }
+}
+
+void RenderBundleEncoder::SetVertexBuffer(uint32_t aSlot, const Buffer& aBuffer,
+ uint64_t aOffset, uint64_t aSize) {
+ if (mValid) {
+ mUsedBuffers.AppendElement(&aBuffer);
+ ffi::wgpu_render_bundle_set_vertex_buffer(mEncoder, aSlot, aBuffer.mId,
+ aOffset, aSize);
+ }
+}
+
+void RenderBundleEncoder::Draw(uint32_t aVertexCount, uint32_t aInstanceCount,
+ uint32_t aFirstVertex, uint32_t aFirstInstance) {
+ if (mValid) {
+ ffi::wgpu_render_bundle_draw(mEncoder, aVertexCount, aInstanceCount,
+ aFirstVertex, aFirstInstance);
+ }
+}
+
+void RenderBundleEncoder::DrawIndexed(uint32_t aIndexCount,
+ uint32_t aInstanceCount,
+ uint32_t aFirstIndex, int32_t aBaseVertex,
+ uint32_t aFirstInstance) {
+ if (mValid) {
+ ffi::wgpu_render_bundle_draw_indexed(mEncoder, aIndexCount, aInstanceCount,
+ aFirstIndex, aBaseVertex,
+ aFirstInstance);
+ }
+}
+
+void RenderBundleEncoder::DrawIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset) {
+ if (mValid) {
+ ffi::wgpu_render_bundle_draw_indirect(mEncoder, aIndirectBuffer.mId,
+ aIndirectOffset);
+ }
+}
+
+void RenderBundleEncoder::DrawIndexedIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset) {
+ if (mValid) {
+ ffi::wgpu_render_bundle_draw_indexed_indirect(mEncoder, aIndirectBuffer.mId,
+ aIndirectOffset);
+ }
+}
+
+void RenderBundleEncoder::PushDebugGroup(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_render_bundle_push_debug_group(mEncoder, utf8.get());
+ }
+}
+void RenderBundleEncoder::PopDebugGroup() {
+ if (mValid) {
+ ffi::wgpu_render_bundle_pop_debug_group(mEncoder);
+ }
+}
+void RenderBundleEncoder::InsertDebugMarker(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_render_bundle_insert_debug_marker(mEncoder, utf8.get());
+ }
+}
+
+already_AddRefed<RenderBundle> RenderBundleEncoder::Finish(
+ const dom::GPURenderBundleDescriptor& aDesc) {
+ RawId id = 0;
+ if (mValid) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->CanSend()) {
+ auto* encoder = mEncoder.forget();
+ MOZ_ASSERT(encoder);
+ id = bridge->RenderBundleEncoderFinish(*encoder, mParent->mId, aDesc);
+ }
+ }
+ RefPtr<RenderBundle> bundle = new RenderBundle(mParent, id);
+ return bundle.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/RenderBundleEncoder.h b/dom/webgpu/RenderBundleEncoder.h
new file mode 100644
index 0000000000..f8bfebe21e
--- /dev/null
+++ b/dom/webgpu/RenderBundleEncoder.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_RenderBundleEncoder_H_
+#define GPU_RenderBundleEncoder_H_
+
+#include "mozilla/Scoped.h"
+#include "mozilla/dom/TypedArray.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+namespace ffi {
+struct WGPURenderBundleEncoder;
+} // namespace ffi
+
+class Device;
+class RenderBundle;
+
+struct ScopedFfiBundleTraits {
+ using type = ffi::WGPURenderBundleEncoder*;
+ static type empty();
+ static void release(type raw);
+};
+
+class RenderBundleEncoder final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(RenderBundleEncoder)
+ GPU_DECL_JS_WRAP(RenderBundleEncoder)
+
+ RenderBundleEncoder(Device* const aParent, WebGPUChild* const aBridge,
+ const dom::GPURenderBundleEncoderDescriptor& aDesc);
+
+ private:
+ ~RenderBundleEncoder();
+ void Cleanup();
+
+ Scoped<ScopedFfiBundleTraits> mEncoder;
+ // keep all the used objects alive while the encoder is finished
+ nsTArray<RefPtr<const BindGroup>> mUsedBindGroups;
+ nsTArray<RefPtr<const Buffer>> mUsedBuffers;
+ nsTArray<RefPtr<const RenderPipeline>> mUsedPipelines;
+ nsTArray<RefPtr<const TextureView>> mUsedTextureViews;
+
+ public:
+ // programmable pass encoder
+ void SetBindGroup(uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets);
+ // render encoder base
+ void SetPipeline(const RenderPipeline& aPipeline);
+ void SetIndexBuffer(const Buffer& aBuffer,
+ const dom::GPUIndexFormat& aIndexFormat, uint64_t aOffset,
+ uint64_t aSize);
+ void SetVertexBuffer(uint32_t aSlot, const Buffer& aBuffer, uint64_t aOffset,
+ uint64_t aSize);
+ void Draw(uint32_t aVertexCount, uint32_t aInstanceCount,
+ uint32_t aFirstVertex, uint32_t aFirstInstance);
+ void DrawIndexed(uint32_t aIndexCount, uint32_t aInstanceCount,
+ uint32_t aFirstIndex, int32_t aBaseVertex,
+ uint32_t aFirstInstance);
+ void DrawIndirect(const Buffer& aIndirectBuffer, uint64_t aIndirectOffset);
+ void DrawIndexedIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset);
+
+ void PushDebugGroup(const nsAString& aString);
+ void PopDebugGroup();
+ void InsertDebugMarker(const nsAString& aString);
+
+ // self
+ already_AddRefed<RenderBundle> Finish(
+ const dom::GPURenderBundleDescriptor& aDesc);
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_RenderBundleEncoder_H_
diff --git a/dom/webgpu/RenderPassEncoder.cpp b/dom/webgpu/RenderPassEncoder.cpp
new file mode 100644
index 0000000000..9f5a311415
--- /dev/null
+++ b/dom/webgpu/RenderPassEncoder.cpp
@@ -0,0 +1,330 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "RenderPassEncoder.h"
+#include "BindGroup.h"
+#include "CommandEncoder.h"
+#include "RenderBundle.h"
+#include "RenderPipeline.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(RenderPassEncoder, mParent, mUsedBindGroups,
+ mUsedBuffers, mUsedPipelines, mUsedTextureViews,
+ mUsedRenderBundles)
+GPU_IMPL_JS_WRAP(RenderPassEncoder)
+
+ffi::WGPURenderPass* ScopedFfiRenderTraits::empty() { return nullptr; }
+
+void ScopedFfiRenderTraits::release(ffi::WGPURenderPass* raw) {
+ if (raw) {
+ ffi::wgpu_render_pass_destroy(raw);
+ }
+}
+
+static ffi::WGPULoadOp ConvertLoadOp(const dom::GPULoadOp& aOp) {
+ switch (aOp) {
+ case dom::GPULoadOp::Load:
+ return ffi::WGPULoadOp_Load;
+ case dom::GPULoadOp::Clear:
+ return ffi::WGPULoadOp_Clear;
+ case dom::GPULoadOp::EndGuard_:
+ break;
+ }
+ MOZ_CRASH("bad GPULoadOp");
+}
+
+static ffi::WGPUStoreOp ConvertStoreOp(const dom::GPUStoreOp& aOp) {
+ switch (aOp) {
+ case dom::GPUStoreOp::Store:
+ return ffi::WGPUStoreOp_Store;
+ case dom::GPUStoreOp::Discard:
+ return ffi::WGPUStoreOp_Discard;
+ case dom::GPUStoreOp::EndGuard_:
+ break;
+ }
+ MOZ_CRASH("bad GPUStoreOp");
+}
+
+static ffi::WGPUColor ConvertColor(const dom::Sequence<double>& aSeq) {
+ ffi::WGPUColor color;
+ color.r = aSeq.SafeElementAt(0, 0.0);
+ color.g = aSeq.SafeElementAt(1, 0.0);
+ color.b = aSeq.SafeElementAt(2, 0.0);
+ color.a = aSeq.SafeElementAt(3, 1.0);
+ return color;
+}
+
+static ffi::WGPUColor ConvertColor(const dom::GPUColorDict& aColor) {
+ ffi::WGPUColor color = {aColor.mR, aColor.mG, aColor.mB, aColor.mA};
+ return color;
+}
+
+static ffi::WGPUColor ConvertColor(
+ const dom::DoubleSequenceOrGPUColorDict& aColor) {
+ if (aColor.IsDoubleSequence()) {
+ return ConvertColor(aColor.GetAsDoubleSequence());
+ }
+ if (aColor.IsGPUColorDict()) {
+ return ConvertColor(aColor.GetAsGPUColorDict());
+ }
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected dom::DoubleSequenceOrGPUColorDict variant");
+ return ffi::WGPUColor();
+}
+static ffi::WGPUColor ConvertColor(
+ const dom::OwningDoubleSequenceOrGPUColorDict& aColor) {
+ if (aColor.IsDoubleSequence()) {
+ return ConvertColor(aColor.GetAsDoubleSequence());
+ }
+ if (aColor.IsGPUColorDict()) {
+ return ConvertColor(aColor.GetAsGPUColorDict());
+ }
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected dom::OwningDoubleSequenceOrGPUColorDict variant");
+ return ffi::WGPUColor();
+}
+
+ffi::WGPURenderPass* BeginRenderPass(
+ CommandEncoder* const aParent, const dom::GPURenderPassDescriptor& aDesc) {
+ ffi::WGPURenderPassDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ffi::WGPURenderPassDepthStencilAttachment dsDesc = {};
+ if (aDesc.mDepthStencilAttachment.WasPassed()) {
+ const auto& dsa = aDesc.mDepthStencilAttachment.Value();
+ dsDesc.view = dsa.mView->mId;
+
+ // -
+
+ if (dsa.mDepthClearValue.WasPassed()) {
+ dsDesc.depth.clear_value = dsa.mDepthClearValue.Value();
+ }
+ if (dsa.mDepthLoadOp.WasPassed()) {
+ dsDesc.depth.load_op = ConvertLoadOp(dsa.mDepthLoadOp.Value());
+ }
+ if (dsa.mDepthStoreOp.WasPassed()) {
+ dsDesc.depth.store_op = ConvertStoreOp(dsa.mDepthStoreOp.Value());
+ }
+ dsDesc.depth.read_only = dsa.mDepthReadOnly;
+
+ // -
+
+ dsDesc.stencil.clear_value = dsa.mStencilClearValue;
+ if (dsa.mStencilLoadOp.WasPassed()) {
+ dsDesc.stencil.load_op = ConvertLoadOp(dsa.mStencilLoadOp.Value());
+ }
+ if (dsa.mStencilStoreOp.WasPassed()) {
+ dsDesc.stencil.store_op = ConvertStoreOp(dsa.mStencilStoreOp.Value());
+ }
+ dsDesc.stencil.read_only = dsa.mStencilReadOnly;
+
+ // -
+
+ desc.depth_stencil_attachment = &dsDesc;
+ }
+
+ if (aDesc.mColorAttachments.Length() > WGPUMAX_COLOR_ATTACHMENTS) {
+ aParent->GetDevice()->GenerateError(nsLiteralCString(
+ "Too many color attachments in GPURenderPassDescriptor"));
+ return nullptr;
+ }
+
+ std::array<ffi::WGPURenderPassColorAttachment, WGPUMAX_COLOR_ATTACHMENTS>
+ colorDescs = {};
+ desc.color_attachments = colorDescs.data();
+ desc.color_attachments_length = aDesc.mColorAttachments.Length();
+
+ for (size_t i = 0; i < aDesc.mColorAttachments.Length(); ++i) {
+ const auto& ca = aDesc.mColorAttachments[i];
+ ffi::WGPURenderPassColorAttachment& cd = colorDescs[i];
+ cd.view = ca.mView->mId;
+ cd.channel.store_op = ConvertStoreOp(ca.mStoreOp);
+
+ if (ca.mResolveTarget.WasPassed()) {
+ cd.resolve_target = ca.mResolveTarget.Value().mId;
+ }
+
+ cd.channel.load_op = ConvertLoadOp(ca.mLoadOp);
+ if (ca.mClearValue.WasPassed()) {
+ cd.channel.clear_value = ConvertColor(ca.mClearValue.Value());
+ }
+ }
+
+ return ffi::wgpu_command_encoder_begin_render_pass(aParent->mId, &desc);
+}
+
+RenderPassEncoder::RenderPassEncoder(CommandEncoder* const aParent,
+ const dom::GPURenderPassDescriptor& aDesc)
+ : ChildOf(aParent), mPass(BeginRenderPass(aParent, aDesc)) {
+ if (!mPass) {
+ mValid = false;
+ return;
+ }
+
+ for (const auto& at : aDesc.mColorAttachments) {
+ mUsedTextureViews.AppendElement(at.mView);
+ }
+ if (aDesc.mDepthStencilAttachment.WasPassed()) {
+ mUsedTextureViews.AppendElement(
+ aDesc.mDepthStencilAttachment.Value().mView);
+ }
+}
+
+RenderPassEncoder::~RenderPassEncoder() {
+ if (mValid) {
+ mValid = false;
+ }
+}
+
+void RenderPassEncoder::SetBindGroup(
+ uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets) {
+ if (mValid) {
+ mUsedBindGroups.AppendElement(&aBindGroup);
+ ffi::wgpu_render_pass_set_bind_group(mPass, aSlot, aBindGroup.mId,
+ aDynamicOffsets.Elements(),
+ aDynamicOffsets.Length());
+ }
+}
+
+void RenderPassEncoder::SetPipeline(const RenderPipeline& aPipeline) {
+ if (mValid) {
+ mUsedPipelines.AppendElement(&aPipeline);
+ ffi::wgpu_render_pass_set_pipeline(mPass, aPipeline.mId);
+ }
+}
+
+void RenderPassEncoder::SetIndexBuffer(const Buffer& aBuffer,
+ const dom::GPUIndexFormat& aIndexFormat,
+ uint64_t aOffset, uint64_t aSize) {
+ if (mValid) {
+ mUsedBuffers.AppendElement(&aBuffer);
+ const auto iformat = aIndexFormat == dom::GPUIndexFormat::Uint32
+ ? ffi::WGPUIndexFormat_Uint32
+ : ffi::WGPUIndexFormat_Uint16;
+ ffi::wgpu_render_pass_set_index_buffer(mPass, aBuffer.mId, iformat, aOffset,
+ aSize);
+ }
+}
+
+void RenderPassEncoder::SetVertexBuffer(uint32_t aSlot, const Buffer& aBuffer,
+ uint64_t aOffset, uint64_t aSize) {
+ if (mValid) {
+ mUsedBuffers.AppendElement(&aBuffer);
+ ffi::wgpu_render_pass_set_vertex_buffer(mPass, aSlot, aBuffer.mId, aOffset,
+ aSize);
+ }
+}
+
+void RenderPassEncoder::Draw(uint32_t aVertexCount, uint32_t aInstanceCount,
+ uint32_t aFirstVertex, uint32_t aFirstInstance) {
+ if (mValid) {
+ ffi::wgpu_render_pass_draw(mPass, aVertexCount, aInstanceCount,
+ aFirstVertex, aFirstInstance);
+ }
+}
+
+void RenderPassEncoder::DrawIndexed(uint32_t aIndexCount,
+ uint32_t aInstanceCount,
+ uint32_t aFirstIndex, int32_t aBaseVertex,
+ uint32_t aFirstInstance) {
+ if (mValid) {
+ ffi::wgpu_render_pass_draw_indexed(mPass, aIndexCount, aInstanceCount,
+ aFirstIndex, aBaseVertex,
+ aFirstInstance);
+ }
+}
+
+void RenderPassEncoder::DrawIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset) {
+ if (mValid) {
+ ffi::wgpu_render_pass_draw_indirect(mPass, aIndirectBuffer.mId,
+ aIndirectOffset);
+ }
+}
+
+void RenderPassEncoder::DrawIndexedIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset) {
+ if (mValid) {
+ ffi::wgpu_render_pass_draw_indexed_indirect(mPass, aIndirectBuffer.mId,
+ aIndirectOffset);
+ }
+}
+
+void RenderPassEncoder::SetViewport(float x, float y, float width, float height,
+ float minDepth, float maxDepth) {
+ if (mValid) {
+ ffi::wgpu_render_pass_set_viewport(mPass, x, y, width, height, minDepth,
+ maxDepth);
+ }
+}
+
+void RenderPassEncoder::SetScissorRect(uint32_t x, uint32_t y, uint32_t width,
+ uint32_t height) {
+ if (mValid) {
+ ffi::wgpu_render_pass_set_scissor_rect(mPass, x, y, width, height);
+ }
+}
+
+void RenderPassEncoder::SetBlendConstant(
+ const dom::DoubleSequenceOrGPUColorDict& color) {
+ if (mValid) {
+ ffi::WGPUColor aColor = ConvertColor(color);
+ ffi::wgpu_render_pass_set_blend_constant(mPass, &aColor);
+ }
+}
+
+void RenderPassEncoder::SetStencilReference(uint32_t reference) {
+ if (mValid) {
+ ffi::wgpu_render_pass_set_stencil_reference(mPass, reference);
+ }
+}
+
+void RenderPassEncoder::ExecuteBundles(
+ const dom::Sequence<OwningNonNull<RenderBundle>>& aBundles) {
+ if (mValid) {
+ nsTArray<ffi::WGPURenderBundleId> renderBundles(aBundles.Length());
+ for (const auto& bundle : aBundles) {
+ mUsedRenderBundles.AppendElement(bundle);
+ renderBundles.AppendElement(bundle->mId);
+ }
+ ffi::wgpu_render_pass_execute_bundles(mPass, renderBundles.Elements(),
+ renderBundles.Length());
+ }
+}
+
+void RenderPassEncoder::PushDebugGroup(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_render_pass_push_debug_group(mPass, utf8.get(), 0);
+ }
+}
+void RenderPassEncoder::PopDebugGroup() {
+ if (mValid) {
+ ffi::wgpu_render_pass_pop_debug_group(mPass);
+ }
+}
+void RenderPassEncoder::InsertDebugMarker(const nsAString& aString) {
+ if (mValid) {
+ const NS_ConvertUTF16toUTF8 utf8(aString);
+ ffi::wgpu_render_pass_insert_debug_marker(mPass, utf8.get(), 0);
+ }
+}
+
+void RenderPassEncoder::End(ErrorResult& aRv) {
+ if (mValid) {
+ mValid = false;
+ auto* pass = mPass.forget();
+ MOZ_ASSERT(pass);
+ mParent->EndRenderPass(*pass, aRv);
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/RenderPassEncoder.h b/dom/webgpu/RenderPassEncoder.h
new file mode 100644
index 0000000000..0255003b22
--- /dev/null
+++ b/dom/webgpu/RenderPassEncoder.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_RenderPassEncoder_H_
+#define GPU_RenderPassEncoder_H_
+
+#include "mozilla/Scoped.h"
+#include "mozilla/dom/TypedArray.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class DoubleSequenceOrGPUColorDict;
+struct GPURenderPassDescriptor;
+template <typename T>
+class Sequence;
+namespace binding_detail {
+template <typename T>
+class AutoSequence;
+} // namespace binding_detail
+} // namespace dom
+namespace webgpu {
+namespace ffi {
+struct WGPURenderPass;
+} // namespace ffi
+
+class BindGroup;
+class Buffer;
+class CommandEncoder;
+class RenderBundle;
+class RenderPipeline;
+class TextureView;
+
+struct ScopedFfiRenderTraits {
+ using type = ffi::WGPURenderPass*;
+ static type empty();
+ static void release(type raw);
+};
+
+class RenderPassEncoder final : public ObjectBase,
+ public ChildOf<CommandEncoder> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(RenderPassEncoder)
+ GPU_DECL_JS_WRAP(RenderPassEncoder)
+
+ RenderPassEncoder(CommandEncoder* const aParent,
+ const dom::GPURenderPassDescriptor& aDesc);
+
+ protected:
+ virtual ~RenderPassEncoder();
+ void Cleanup() {}
+
+ Scoped<ScopedFfiRenderTraits> mPass;
+ // keep all the used objects alive while the pass is recorded
+ nsTArray<RefPtr<const BindGroup>> mUsedBindGroups;
+ nsTArray<RefPtr<const Buffer>> mUsedBuffers;
+ nsTArray<RefPtr<const RenderPipeline>> mUsedPipelines;
+ nsTArray<RefPtr<const TextureView>> mUsedTextureViews;
+ nsTArray<RefPtr<const RenderBundle>> mUsedRenderBundles;
+
+ public:
+ // programmable pass encoder
+ void SetBindGroup(uint32_t aSlot, const BindGroup& aBindGroup,
+ const dom::Sequence<uint32_t>& aDynamicOffsets);
+ // render encoder base
+ void SetPipeline(const RenderPipeline& aPipeline);
+ void SetIndexBuffer(const Buffer& aBuffer,
+ const dom::GPUIndexFormat& aIndexFormat, uint64_t aOffset,
+ uint64_t aSize);
+ void SetVertexBuffer(uint32_t aSlot, const Buffer& aBuffer, uint64_t aOffset,
+ uint64_t aSize);
+ void Draw(uint32_t aVertexCount, uint32_t aInstanceCount,
+ uint32_t aFirstVertex, uint32_t aFirstInstance);
+ void DrawIndexed(uint32_t aIndexCount, uint32_t aInstanceCount,
+ uint32_t aFirstIndex, int32_t aBaseVertex,
+ uint32_t aFirstInstance);
+ void DrawIndirect(const Buffer& aIndirectBuffer, uint64_t aIndirectOffset);
+ void DrawIndexedIndirect(const Buffer& aIndirectBuffer,
+ uint64_t aIndirectOffset);
+ // self
+ void SetViewport(float x, float y, float width, float height, float minDepth,
+ float maxDepth);
+ void SetScissorRect(uint32_t x, uint32_t y, uint32_t width, uint32_t height);
+ void SetBlendConstant(const dom::DoubleSequenceOrGPUColorDict& color);
+ void SetStencilReference(uint32_t reference);
+
+ void PushDebugGroup(const nsAString& aString);
+ void PopDebugGroup();
+ void InsertDebugMarker(const nsAString& aString);
+
+ void ExecuteBundles(
+ const dom::Sequence<OwningNonNull<RenderBundle>>& aBundles);
+
+ void End(ErrorResult& aRv);
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_RenderPassEncoder_H_
diff --git a/dom/webgpu/RenderPipeline.cpp b/dom/webgpu/RenderPipeline.cpp
new file mode 100644
index 0000000000..d99ac87c4a
--- /dev/null
+++ b/dom/webgpu/RenderPipeline.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RenderPipeline.h"
+
+#include "Device.h"
+#include "ipc/WebGPUChild.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(RenderPipeline, mParent)
+GPU_IMPL_JS_WRAP(RenderPipeline)
+
+RenderPipeline::RenderPipeline(Device* const aParent, RawId aId,
+ RawId aImplicitPipelineLayoutId,
+ nsTArray<RawId>&& aImplicitBindGroupLayoutIds)
+ : ChildOf(aParent),
+ mImplicitPipelineLayoutId(aImplicitPipelineLayoutId),
+ mImplicitBindGroupLayoutIds(std::move(aImplicitBindGroupLayoutIds)),
+ mId(aId) {}
+
+RenderPipeline::~RenderPipeline() { Cleanup(); }
+
+void RenderPipeline::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendRenderPipelineDestroy(mId);
+ if (mImplicitPipelineLayoutId) {
+ bridge->SendImplicitLayoutDestroy(mImplicitPipelineLayoutId,
+ mImplicitBindGroupLayoutIds);
+ }
+ }
+ }
+}
+
+already_AddRefed<BindGroupLayout> RenderPipeline::GetBindGroupLayout(
+ uint32_t index) const {
+ const RawId id = index < mImplicitBindGroupLayoutIds.Length()
+ ? mImplicitBindGroupLayoutIds[index]
+ : 0;
+ RefPtr<BindGroupLayout> object = new BindGroupLayout(mParent, id, false);
+ return object.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/RenderPipeline.h b/dom/webgpu/RenderPipeline.h
new file mode 100644
index 0000000000..859259da27
--- /dev/null
+++ b/dom/webgpu/RenderPipeline.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_RenderPipeline_H_
+#define GPU_RenderPipeline_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsTArray.h"
+
+namespace mozilla::webgpu {
+
+class BindGroupLayout;
+class Device;
+
+class RenderPipeline final : public ObjectBase, public ChildOf<Device> {
+ const RawId mImplicitPipelineLayoutId;
+ const nsTArray<RawId> mImplicitBindGroupLayoutIds;
+
+ public:
+ GPU_DECL_CYCLE_COLLECTION(RenderPipeline)
+ GPU_DECL_JS_WRAP(RenderPipeline)
+
+ const RawId mId;
+
+ RenderPipeline(Device* const aParent, RawId aId,
+ RawId aImplicitPipelineLayoutId,
+ nsTArray<RawId>&& aImplicitBindGroupLayoutIds);
+ already_AddRefed<BindGroupLayout> GetBindGroupLayout(uint32_t index) const;
+
+ private:
+ virtual ~RenderPipeline();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_RenderPipeline_H_
diff --git a/dom/webgpu/Sampler.cpp b/dom/webgpu/Sampler.cpp
new file mode 100644
index 0000000000..9a3b90c4dd
--- /dev/null
+++ b/dom/webgpu/Sampler.cpp
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "Sampler.h"
+#include "ipc/WebGPUChild.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(Sampler, mParent)
+GPU_IMPL_JS_WRAP(Sampler)
+
+Sampler::Sampler(Device* const aParent, RawId aId)
+ : ChildOf(aParent), mId(aId) {}
+
+Sampler::~Sampler() { Cleanup(); }
+
+void Sampler::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendSamplerDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Sampler.h b/dom/webgpu/Sampler.h
new file mode 100644
index 0000000000..02e01982cd
--- /dev/null
+++ b/dom/webgpu/Sampler.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_SAMPLER_H_
+#define GPU_SAMPLER_H_
+
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+
+class Device;
+
+class Sampler final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(Sampler)
+ GPU_DECL_JS_WRAP(Sampler)
+
+ Sampler(Device* const aParent, RawId aId);
+
+ const RawId mId;
+
+ private:
+ virtual ~Sampler();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_SAMPLER_H_
diff --git a/dom/webgpu/ShaderModule.cpp b/dom/webgpu/ShaderModule.cpp
new file mode 100644
index 0000000000..c415fa732a
--- /dev/null
+++ b/dom/webgpu/ShaderModule.cpp
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "ShaderModule.h"
+#include "CompilationInfo.h"
+#include "ipc/WebGPUChild.h"
+
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(ShaderModule, mParent)
+GPU_IMPL_JS_WRAP(ShaderModule)
+
+ShaderModule::ShaderModule(Device* const aParent, RawId aId,
+ const RefPtr<dom::Promise>& aCompilationInfo)
+ : ChildOf(aParent), mId(aId), mCompilationInfo(aCompilationInfo) {}
+
+ShaderModule::~ShaderModule() { Cleanup(); }
+
+void ShaderModule::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendShaderModuleDestroy(mId);
+ }
+ }
+}
+
+already_AddRefed<dom::Promise> ShaderModule::CompilationInfo(ErrorResult& aRv) {
+ RefPtr<dom::Promise> tmp = mCompilationInfo;
+ return tmp.forget();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ShaderModule.h b/dom/webgpu/ShaderModule.h
new file mode 100644
index 0000000000..08eaf2b804
--- /dev/null
+++ b/dom/webgpu/ShaderModule.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_ShaderModule_H_
+#define GPU_ShaderModule_H_
+
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+
+class CompilationInfo;
+class Device;
+
+class ShaderModule final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(
+ ShaderModule) // TODO: kvark's WIP patch was passing CompilationInfo as a
+ // second argument here.
+ GPU_DECL_JS_WRAP(ShaderModule)
+
+ ShaderModule(Device* const aParent, RawId aId,
+ const RefPtr<dom::Promise>& aCompilationInfo);
+ already_AddRefed<dom::Promise> CompilationInfo(ErrorResult& aRv);
+
+ const RawId mId;
+
+ private:
+ virtual ~ShaderModule();
+ void Cleanup();
+
+ RefPtr<dom::Promise> mCompilationInfo;
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_ShaderModule_H_
diff --git a/dom/webgpu/SupportedFeatures.cpp b/dom/webgpu/SupportedFeatures.cpp
new file mode 100644
index 0000000000..072705892e
--- /dev/null
+++ b/dom/webgpu/SupportedFeatures.cpp
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SupportedFeatures.h"
+#include "Adapter.h"
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(SupportedFeatures, mParent)
+GPU_IMPL_JS_WRAP(SupportedFeatures)
+
+SupportedFeatures::SupportedFeatures(Adapter* const aParent)
+ : ChildOf(aParent) {}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/SupportedFeatures.h b/dom/webgpu/SupportedFeatures.h
new file mode 100644
index 0000000000..5c12ac8d3c
--- /dev/null
+++ b/dom/webgpu/SupportedFeatures.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_SupportedFeatures_H_
+#define GPU_SupportedFeatures_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+class Adapter;
+
+class SupportedFeatures final : public nsWrapperCache, public ChildOf<Adapter> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(SupportedFeatures)
+ GPU_DECL_JS_WRAP(SupportedFeatures)
+
+ explicit SupportedFeatures(Adapter* const aParent);
+
+ private:
+ ~SupportedFeatures() = default;
+ void Cleanup() {}
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_SupportedFeatures_H_
diff --git a/dom/webgpu/SupportedLimits.cpp b/dom/webgpu/SupportedLimits.cpp
new file mode 100644
index 0000000000..ea37dec206
--- /dev/null
+++ b/dom/webgpu/SupportedLimits.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SupportedLimits.h"
+#include "Adapter.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(SupportedLimits, mParent)
+GPU_IMPL_JS_WRAP(SupportedLimits)
+
+SupportedLimits::SupportedLimits(Adapter* const aParent,
+ UniquePtr<ffi::WGPULimits>&& aLimits)
+ : ChildOf(aParent), mLimits(std::move(aLimits)) {}
+
+SupportedLimits::~SupportedLimits() = default;
+
+uint32_t SupportedLimits::MaxTextureDimension1D() const {
+ return mLimits->max_texture_dimension_1d;
+}
+uint32_t SupportedLimits::MaxTextureDimension2D() const {
+ return mLimits->max_texture_dimension_2d;
+}
+uint32_t SupportedLimits::MaxTextureDimension3D() const {
+ return mLimits->max_texture_dimension_3d;
+}
+uint32_t SupportedLimits::MaxTextureArrayLayers() const {
+ return mLimits->max_texture_array_layers;
+}
+uint32_t SupportedLimits::MaxBindGroups() const {
+ return mLimits->max_bind_groups;
+}
+uint32_t SupportedLimits::MaxDynamicUniformBuffersPerPipelineLayout() const {
+ return mLimits->max_dynamic_uniform_buffers_per_pipeline_layout;
+}
+uint32_t SupportedLimits::MaxDynamicStorageBuffersPerPipelineLayout() const {
+ return mLimits->max_dynamic_storage_buffers_per_pipeline_layout;
+}
+uint32_t SupportedLimits::MaxSampledTexturesPerShaderStage() const {
+ return mLimits->max_sampled_textures_per_shader_stage;
+}
+uint32_t SupportedLimits::MaxSamplersPerShaderStage() const {
+ return mLimits->max_samplers_per_shader_stage;
+}
+uint32_t SupportedLimits::MaxStorageBuffersPerShaderStage() const {
+ return mLimits->max_storage_buffers_per_shader_stage;
+}
+uint32_t SupportedLimits::MaxStorageTexturesPerShaderStage() const {
+ return mLimits->max_storage_textures_per_shader_stage;
+}
+uint32_t SupportedLimits::MaxUniformBuffersPerShaderStage() const {
+ return mLimits->max_uniform_buffers_per_shader_stage;
+}
+uint32_t SupportedLimits::MaxUniformBufferBindingSize() const {
+ return mLimits->max_uniform_buffer_binding_size;
+}
+uint32_t SupportedLimits::MaxStorageBufferBindingSize() const {
+ return mLimits->max_storage_buffer_binding_size;
+}
+uint32_t SupportedLimits::MinUniformBufferOffsetAlignment() const {
+ return mLimits->min_uniform_buffer_offset_alignment;
+}
+uint32_t SupportedLimits::MinStorageBufferOffsetAlignment() const {
+ return mLimits->min_storage_buffer_offset_alignment;
+}
+uint32_t SupportedLimits::MaxVertexBuffers() const {
+ return mLimits->max_vertex_buffers;
+}
+uint32_t SupportedLimits::MaxVertexAttributes() const {
+ return mLimits->max_vertex_attributes;
+}
+uint32_t SupportedLimits::MaxVertexBufferArrayStride() const {
+ return mLimits->max_vertex_buffer_array_stride;
+}
+uint32_t SupportedLimits::MaxInterStageShaderComponents() const {
+ return mLimits->max_inter_stage_shader_components;
+}
+uint32_t SupportedLimits::MaxComputeWorkgroupStorageSize() const {
+ return mLimits->max_compute_workgroup_storage_size;
+}
+uint32_t SupportedLimits::MaxComputeInvocationsPerWorkgroup() const {
+ return mLimits->max_compute_invocations_per_workgroup;
+}
+uint32_t SupportedLimits::MaxComputeWorkgroupSizeX() const {
+ return mLimits->max_compute_workgroup_size_x;
+}
+uint32_t SupportedLimits::MaxComputeWorkgroupSizeY() const {
+ return mLimits->max_compute_workgroup_size_y;
+}
+uint32_t SupportedLimits::MaxComputeWorkgroupSizeZ() const {
+ return mLimits->max_compute_workgroup_size_z;
+}
+uint32_t SupportedLimits::MaxComputeWorkgroupsPerDimension() const {
+ return mLimits->max_compute_workgroups_per_dimension;
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/SupportedLimits.h b/dom/webgpu/SupportedLimits.h
new file mode 100644
index 0000000000..3c38ae6343
--- /dev/null
+++ b/dom/webgpu/SupportedLimits.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_SupportedLimits_H_
+#define GPU_SupportedLimits_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla::webgpu {
+namespace ffi {
+struct WGPULimits;
+}
+class Adapter;
+
+class SupportedLimits final : public nsWrapperCache, public ChildOf<Adapter> {
+ const UniquePtr<ffi::WGPULimits> mLimits;
+
+ public:
+ GPU_DECL_CYCLE_COLLECTION(SupportedLimits)
+ GPU_DECL_JS_WRAP(SupportedLimits)
+
+ uint32_t MaxTextureDimension1D() const;
+ uint32_t MaxTextureDimension2D() const;
+ uint32_t MaxTextureDimension3D() const;
+ uint32_t MaxTextureArrayLayers() const;
+ uint32_t MaxBindGroups() const;
+ uint32_t MaxDynamicUniformBuffersPerPipelineLayout() const;
+ uint32_t MaxDynamicStorageBuffersPerPipelineLayout() const;
+ uint32_t MaxSampledTexturesPerShaderStage() const;
+ uint32_t MaxSamplersPerShaderStage() const;
+ uint32_t MaxStorageBuffersPerShaderStage() const;
+ uint32_t MaxStorageTexturesPerShaderStage() const;
+ uint32_t MaxUniformBuffersPerShaderStage() const;
+ uint32_t MaxUniformBufferBindingSize() const;
+ uint32_t MaxStorageBufferBindingSize() const;
+ uint32_t MinUniformBufferOffsetAlignment() const;
+ uint32_t MinStorageBufferOffsetAlignment() const;
+ uint32_t MaxVertexBuffers() const;
+ uint32_t MaxVertexAttributes() const;
+ uint32_t MaxVertexBufferArrayStride() const;
+ uint32_t MaxInterStageShaderComponents() const;
+ uint32_t MaxComputeWorkgroupStorageSize() const;
+ uint32_t MaxComputeInvocationsPerWorkgroup() const;
+ uint32_t MaxComputeWorkgroupSizeX() const;
+ uint32_t MaxComputeWorkgroupSizeY() const;
+ uint32_t MaxComputeWorkgroupSizeZ() const;
+ uint32_t MaxComputeWorkgroupsPerDimension() const;
+
+ SupportedLimits(Adapter* const aParent, UniquePtr<ffi::WGPULimits>&& aLimits);
+
+ private:
+ ~SupportedLimits();
+ void Cleanup() {}
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_SupportedLimits_H_
diff --git a/dom/webgpu/Texture.cpp b/dom/webgpu/Texture.cpp
new file mode 100644
index 0000000000..e80710647e
--- /dev/null
+++ b/dom/webgpu/Texture.cpp
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Texture.h"
+
+#include "ipc/WebGPUChild.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "mozilla/webgpu/CanvasContext.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "TextureView.h"
+#include "Utility.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(Texture, mParent)
+GPU_IMPL_JS_WRAP(Texture)
+
+static Maybe<uint8_t> GetBytesPerBlock(dom::GPUTextureFormat format) {
+ switch (format) {
+ case dom::GPUTextureFormat::R8unorm:
+ case dom::GPUTextureFormat::R8snorm:
+ case dom::GPUTextureFormat::R8uint:
+ case dom::GPUTextureFormat::R8sint:
+ return Some<uint8_t>(1u);
+ case dom::GPUTextureFormat::R16uint:
+ case dom::GPUTextureFormat::R16sint:
+ case dom::GPUTextureFormat::R16float:
+ case dom::GPUTextureFormat::Rg8unorm:
+ case dom::GPUTextureFormat::Rg8snorm:
+ case dom::GPUTextureFormat::Rg8uint:
+ case dom::GPUTextureFormat::Rg8sint:
+ return Some<uint8_t>(2u);
+ case dom::GPUTextureFormat::R32uint:
+ case dom::GPUTextureFormat::R32sint:
+ case dom::GPUTextureFormat::R32float:
+ case dom::GPUTextureFormat::Rg16uint:
+ case dom::GPUTextureFormat::Rg16sint:
+ case dom::GPUTextureFormat::Rg16float:
+ case dom::GPUTextureFormat::Rgba8unorm:
+ case dom::GPUTextureFormat::Rgba8unorm_srgb:
+ case dom::GPUTextureFormat::Rgba8snorm:
+ case dom::GPUTextureFormat::Rgba8uint:
+ case dom::GPUTextureFormat::Rgba8sint:
+ case dom::GPUTextureFormat::Bgra8unorm:
+ case dom::GPUTextureFormat::Bgra8unorm_srgb:
+ case dom::GPUTextureFormat::Rgb9e5ufloat:
+ case dom::GPUTextureFormat::Rgb10a2unorm:
+ case dom::GPUTextureFormat::Rg11b10float:
+ return Some<uint8_t>(4u);
+ case dom::GPUTextureFormat::Rg32uint:
+ case dom::GPUTextureFormat::Rg32sint:
+ case dom::GPUTextureFormat::Rg32float:
+ case dom::GPUTextureFormat::Rgba16uint:
+ case dom::GPUTextureFormat::Rgba16sint:
+ case dom::GPUTextureFormat::Rgba16float:
+ return Some<uint8_t>(8u);
+ case dom::GPUTextureFormat::Rgba32uint:
+ case dom::GPUTextureFormat::Rgba32sint:
+ case dom::GPUTextureFormat::Rgba32float:
+ return Some<uint8_t>(16u);
+ case dom::GPUTextureFormat::Depth32float:
+ return Some<uint8_t>(4u);
+ case dom::GPUTextureFormat::Bc1_rgba_unorm:
+ case dom::GPUTextureFormat::Bc1_rgba_unorm_srgb:
+ case dom::GPUTextureFormat::Bc4_r_unorm:
+ case dom::GPUTextureFormat::Bc4_r_snorm:
+ return Some<uint8_t>(8u);
+ case dom::GPUTextureFormat::Bc2_rgba_unorm:
+ case dom::GPUTextureFormat::Bc2_rgba_unorm_srgb:
+ case dom::GPUTextureFormat::Bc3_rgba_unorm:
+ case dom::GPUTextureFormat::Bc3_rgba_unorm_srgb:
+ case dom::GPUTextureFormat::Bc5_rg_unorm:
+ case dom::GPUTextureFormat::Bc5_rg_snorm:
+ case dom::GPUTextureFormat::Bc6h_rgb_ufloat:
+ case dom::GPUTextureFormat::Bc6h_rgb_float:
+ case dom::GPUTextureFormat::Bc7_rgba_unorm:
+ case dom::GPUTextureFormat::Bc7_rgba_unorm_srgb:
+ return Some<uint8_t>(16u);
+ case dom::GPUTextureFormat::Depth24plus:
+ case dom::GPUTextureFormat::Depth24plus_stencil8:
+ case dom::GPUTextureFormat::Depth32float_stencil8:
+ case dom::GPUTextureFormat::EndGuard_:
+ return Nothing();
+ }
+ return Nothing();
+}
+
+Texture::Texture(Device* const aParent, RawId aId,
+ const dom::GPUTextureDescriptor& aDesc)
+ : ChildOf(aParent),
+ mId(aId),
+ mFormat(aDesc.mFormat),
+ mBytesPerBlock(GetBytesPerBlock(aDesc.mFormat)),
+ mSize(ConvertExtent(aDesc.mSize)),
+ mMipLevelCount(aDesc.mMipLevelCount),
+ mSampleCount(aDesc.mSampleCount),
+ mDimension(aDesc.mDimension),
+ mUsage(aDesc.mUsage) {}
+
+Texture::~Texture() { Cleanup(); }
+
+void Texture::Cleanup() {
+ if (mValid && mParent) {
+ mValid = false;
+ auto bridge = mParent->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendTextureDestroy(mId);
+ }
+ }
+}
+
+already_AddRefed<TextureView> Texture::CreateView(
+ const dom::GPUTextureViewDescriptor& aDesc) {
+ auto bridge = mParent->GetBridge();
+ RawId id = 0;
+ if (bridge->IsOpen()) {
+ id = bridge->TextureCreateView(mId, mParent->mId, aDesc);
+ }
+
+ RefPtr<TextureView> view = new TextureView(this, id);
+ return view.forget();
+}
+
+void Texture::Destroy() {
+ // TODO: we don't have to implement it right now, but it's used by the
+ // examples
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Texture.h b/dom/webgpu/Texture.h
new file mode 100644
index 0000000000..e31878f825
--- /dev/null
+++ b/dom/webgpu/Texture.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_Texture_H_
+#define GPU_Texture_H_
+
+#include <cstdint>
+#include "mozilla/WeakPtr.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla {
+namespace dom {
+struct GPUTextureDescriptor;
+struct GPUTextureViewDescriptor;
+enum class GPUTextureDimension : uint8_t;
+enum class GPUTextureFormat : uint8_t;
+enum class GPUTextureUsageFlags : uint32_t;
+} // namespace dom
+
+namespace webgpu {
+
+class CanvasContext;
+class Device;
+class TextureView;
+
+class Texture final : public ObjectBase, public ChildOf<Device> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(Texture)
+ GPU_DECL_JS_WRAP(Texture)
+
+ Texture(Device* const aParent, RawId aId,
+ const dom::GPUTextureDescriptor& aDesc);
+ Device* GetParentDevice() { return mParent; }
+ const RawId mId;
+ const dom::GPUTextureFormat mFormat;
+ const Maybe<uint8_t> mBytesPerBlock;
+
+ WeakPtr<CanvasContext> mTargetContext;
+
+ private:
+ virtual ~Texture();
+ void Cleanup();
+
+ const ffi::WGPUExtent3d mSize;
+ const uint32_t mMipLevelCount;
+ const uint32_t mSampleCount;
+ const dom::GPUTextureDimension mDimension;
+ const uint32_t mUsage;
+
+ public:
+ already_AddRefed<TextureView> CreateView(
+ const dom::GPUTextureViewDescriptor& aDesc);
+ void Destroy();
+
+ uint32_t Width() const { return mSize.width; }
+ uint32_t Height() const { return mSize.height; }
+ uint32_t DepthOrArrayLayers() const { return mSize.depth_or_array_layers; }
+ uint32_t MipLevelCount() const { return mMipLevelCount; }
+ uint32_t SampleCount() const { return mSampleCount; }
+ dom::GPUTextureDimension Dimension() const { return mDimension; }
+ dom::GPUTextureFormat Format() const { return mFormat; }
+ uint32_t Usage() const { return mUsage; }
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_Texture_H_
diff --git a/dom/webgpu/TextureView.cpp b/dom/webgpu/TextureView.cpp
new file mode 100644
index 0000000000..4401155b90
--- /dev/null
+++ b/dom/webgpu/TextureView.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureView.h"
+
+#include "Device.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/webgpu/CanvasContext.h"
+#include "ipc/WebGPUChild.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(TextureView, mParent)
+GPU_IMPL_JS_WRAP(TextureView)
+
+TextureView::TextureView(Texture* const aParent, RawId aId)
+ : ChildOf(aParent), mId(aId) {}
+
+TextureView::~TextureView() { Cleanup(); }
+
+CanvasContext* TextureView::GetTargetContext() const {
+ return mParent->mTargetContext;
+} // namespace webgpu
+
+void TextureView::Cleanup() {
+ if (mValid && mParent && mParent->GetParentDevice()) {
+ mValid = false;
+ auto bridge = mParent->GetParentDevice()->GetBridge();
+ if (bridge && bridge->IsOpen()) {
+ bridge->SendTextureViewDestroy(mId);
+ }
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/TextureView.h b/dom/webgpu/TextureView.h
new file mode 100644
index 0000000000..a0c69c106b
--- /dev/null
+++ b/dom/webgpu/TextureView.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_TextureView_H_
+#define GPU_TextureView_H_
+
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla::webgpu {
+
+class CanvasContext;
+class Texture;
+
+class TextureView final : public ObjectBase, public ChildOf<Texture> {
+ public:
+ GPU_DECL_CYCLE_COLLECTION(TextureView)
+ GPU_DECL_JS_WRAP(TextureView)
+
+ TextureView(Texture* const aParent, RawId aId);
+ CanvasContext* GetTargetContext() const;
+
+ const RawId mId;
+
+ private:
+ virtual ~TextureView();
+ void Cleanup();
+};
+
+} // namespace mozilla::webgpu
+
+#endif // GPU_TextureView_H_
diff --git a/dom/webgpu/Utility.cpp b/dom/webgpu/Utility.cpp
new file mode 100644
index 0000000000..8c8a03866b
--- /dev/null
+++ b/dom/webgpu/Utility.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Utility.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+
+namespace mozilla::webgpu {
+
+template <typename E>
+void ConvertToExtent3D(const E& aExtent, ffi::WGPUExtent3d* aExtentFFI) {
+ *aExtentFFI = {};
+ if (aExtent.IsRangeEnforcedUnsignedLongSequence()) {
+ const auto& seq = aExtent.GetAsRangeEnforcedUnsignedLongSequence();
+ aExtentFFI->width = seq.Length() > 0 ? seq[0] : 0;
+ aExtentFFI->height = seq.Length() > 1 ? seq[1] : 1;
+ aExtentFFI->depth_or_array_layers = seq.Length() > 2 ? seq[2] : 1;
+ } else if (aExtent.IsGPUExtent3DDict()) {
+ const auto& dict = aExtent.GetAsGPUExtent3DDict();
+ aExtentFFI->width = dict.mWidth;
+ aExtentFFI->height = dict.mHeight;
+ aExtentFFI->depth_or_array_layers = dict.mDepthOrArrayLayers;
+ } else {
+ MOZ_CRASH("Unexpected extent type");
+ }
+}
+
+void ConvertExtent3DToFFI(const dom::GPUExtent3D& aExtent,
+ ffi::WGPUExtent3d* aExtentFFI) {
+ ConvertToExtent3D(aExtent, aExtentFFI);
+}
+
+void ConvertExtent3DToFFI(const dom::OwningGPUExtent3D& aExtent,
+ ffi::WGPUExtent3d* aExtentFFI) {
+ ConvertToExtent3D(aExtent, aExtentFFI);
+}
+
+ffi::WGPUExtent3d ConvertExtent(const dom::GPUExtent3D& aExtent) {
+ ffi::WGPUExtent3d extent = {};
+ ConvertToExtent3D(aExtent, &extent);
+ return extent;
+}
+
+ffi::WGPUExtent3d ConvertExtent(const dom::OwningGPUExtent3D& aExtent) {
+ ffi::WGPUExtent3d extent = {};
+ ConvertToExtent3D(aExtent, &extent);
+ return extent;
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/Utility.h b/dom/webgpu/Utility.h
new file mode 100644
index 0000000000..7d2b82484d
--- /dev/null
+++ b/dom/webgpu/Utility.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_UTIL_H_
+#define GPU_UTIL_H_
+
+#include "mozilla/dom/WebGPUBinding.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+struct GPUComputePassDescriptor;
+template <typename T>
+class Sequence;
+using GPUExtent3D = RangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+using OwningGPUExtent3D =
+ OwningRangeEnforcedUnsignedLongSequenceOrGPUExtent3DDict;
+} // namespace dom
+namespace webgpu {
+namespace ffi {
+struct WGPUExtent3d;
+} // namespace ffi
+
+void ConvertExtent3DToFFI(const dom::GPUExtent3D& aExtent,
+ ffi::WGPUExtent3d* aExtentFFI);
+
+void ConvertExtent3DToFFI(const dom::OwningGPUExtent3D& aExtent,
+ ffi::WGPUExtent3d* aExtentFFI);
+
+ffi::WGPUExtent3d ConvertExtent(const dom::GPUExtent3D& aExtent);
+
+ffi::WGPUExtent3d ConvertExtent(const dom::OwningGPUExtent3D& aExtent);
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_UTIL_H_
diff --git a/dom/webgpu/ValidationError.cpp b/dom/webgpu/ValidationError.cpp
new file mode 100644
index 0000000000..119156713c
--- /dev/null
+++ b/dom/webgpu/ValidationError.cpp
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ValidationError.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIGlobalObject.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_CYCLE_COLLECTION(ValidationError, mGlobal)
+GPU_IMPL_JS_WRAP(ValidationError)
+
+ValidationError::ValidationError(nsIGlobalObject* aGlobal,
+ const nsACString& aMessage)
+ : mGlobal(aGlobal) {
+ CopyUTF8toUTF16(aMessage, mMessage);
+}
+
+ValidationError::ValidationError(nsIGlobalObject* aGlobal,
+ const nsAString& aMessage)
+ : mGlobal(aGlobal), mMessage(aMessage) {}
+
+ValidationError::~ValidationError() = default;
+
+already_AddRefed<ValidationError> ValidationError::Constructor(
+ const dom::GlobalObject& aGlobal, const nsAString& aString,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.ThrowInvalidStateError("aGlobal is not nsIGlobalObject");
+ return nullptr;
+ }
+
+ return MakeAndAddRef<ValidationError>(global, aString);
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ValidationError.h b/dom/webgpu/ValidationError.h
new file mode 100644
index 0000000000..bdbc1ce2eb
--- /dev/null
+++ b/dom/webgpu/ValidationError.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GPU_ValidationError_H_
+#define GPU_ValidationError_H_
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "ObjectModel.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class GlobalObject;
+} // namespace dom
+namespace webgpu {
+
+class ValidationError final : public nsWrapperCache {
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ nsString mMessage;
+
+ public:
+ GPU_DECL_CYCLE_COLLECTION(ValidationError)
+ GPU_DECL_JS_WRAP(ValidationError)
+ ValidationError(nsIGlobalObject* aGlobal, const nsACString& aMessage);
+ ValidationError(nsIGlobalObject* aGlobal, const nsAString& aMessage);
+
+ private:
+ virtual ~ValidationError();
+ void Cleanup() {}
+
+ public:
+ static already_AddRefed<ValidationError> Constructor(
+ const dom::GlobalObject& aGlobal, const nsAString& aString,
+ ErrorResult& aRv);
+ void GetMessage(nsAString& aMessage) const { aMessage = mMessage; }
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // GPU_ValidationError_H_
diff --git a/dom/webgpu/ipc/PWebGPU.ipdl b/dom/webgpu/ipc/PWebGPU.ipdl
new file mode 100644
index 0000000000..3bdc49fa4d
--- /dev/null
+++ b/dom/webgpu/ipc/PWebGPU.ipdl
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+using mozilla::layers::RGBDescriptor from "mozilla/layers/LayersSurfaces.h";
+using mozilla::layers::RemoteTextureId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::RemoteTextureOwnerId from "mozilla/layers/LayersTypes.h";
+using mozilla::webgpu::RawId from "mozilla/webgpu/WebGPUTypes.h";
+using mozilla::dom::GPURequestAdapterOptions from "mozilla/dom/WebGPUBinding.h";
+using mozilla::dom::GPUCommandBufferDescriptor from "mozilla/dom/WebGPUBinding.h";
+using mozilla::dom::GPUBufferDescriptor from "mozilla/dom/WebGPUBinding.h";
+using mozilla::webgpu::MaybeScopedError from "mozilla/webgpu/WebGPUTypes.h";
+using mozilla::webgpu::WebGPUCompilationMessage from "mozilla/webgpu/WebGPUTypes.h";
+[MoveOnly] using class mozilla::ipc::UnsafeSharedMemoryHandle from "mozilla/ipc/RawShmem.h";
+
+include "mozilla/ipc/ByteBufUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/webgpu/WebGPUSerialize.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+include protocol PCanvasManager;
+include PWebGPUTypes;
+
+namespace mozilla {
+namespace webgpu {
+
+/**
+ * Represents the connection between a WebGPUChild actor that issues WebGPU
+ * command from the content process, and a WebGPUParent in the compositor
+ * process that runs the commands.
+ */
+async protocol PWebGPU
+{
+ manager PCanvasManager;
+
+parent:
+ async DeviceAction(RawId selfId, ByteBuf buf);
+ async DeviceActionWithAck(RawId selfId, ByteBuf buf) returns (bool dummy);
+ async TextureAction(RawId selfId, RawId aDeviceId, ByteBuf buf);
+ async CommandEncoderAction(RawId selfId, RawId aDeviceId, ByteBuf buf);
+ async BumpImplicitBindGroupLayout(RawId pipelineId, bool isCompute, uint32_t index, RawId assignId);
+
+ async CreateBuffer(RawId deviceId, RawId bufferId, GPUBufferDescriptor desc, UnsafeSharedMemoryHandle shm);
+
+ async InstanceRequestAdapter(GPURequestAdapterOptions options, RawId[] ids) returns (ByteBuf byteBuf);
+ async AdapterRequestDevice(RawId selfId, ByteBuf buf, RawId newId) returns (bool success);
+ async AdapterDestroy(RawId selfId);
+ // TODO: We want to return an array of compilation messages.
+ async DeviceCreateShaderModule(RawId selfId, RawId bufferId, nsString label, nsCString code) returns (WebGPUCompilationMessage[] messages);
+ async BufferMap(RawId selfId, uint32_t aMode, uint64_t offset, uint64_t size) returns (BufferMapResult result);
+ async BufferUnmap(RawId deviceId, RawId bufferId, bool flush);
+ async BufferDestroy(RawId selfId);
+ async BufferDrop(RawId selfId);
+ async TextureDestroy(RawId selfId);
+ async TextureViewDestroy(RawId selfId);
+ async SamplerDestroy(RawId selfId);
+ async DeviceDestroy(RawId selfId);
+
+ async CommandEncoderFinish(RawId selfId, RawId deviceId, GPUCommandBufferDescriptor desc);
+ async CommandEncoderDestroy(RawId selfId);
+ async CommandBufferDestroy(RawId selfId);
+ async RenderBundleDestroy(RawId selfId);
+ async QueueSubmit(RawId selfId, RawId aDeviceId, RawId[] commandBuffers);
+ async QueueWriteAction(RawId selfId, RawId aDeviceId, ByteBuf buf, UnsafeSharedMemoryHandle shmem);
+
+ async BindGroupLayoutDestroy(RawId selfId);
+ async PipelineLayoutDestroy(RawId selfId);
+ async BindGroupDestroy(RawId selfId);
+ async ShaderModuleDestroy(RawId selfId);
+ async ComputePipelineDestroy(RawId selfId);
+ async RenderPipelineDestroy(RawId selfId);
+ async ImplicitLayoutDestroy(RawId implicitPlId, RawId[] implicitBglIds);
+ async DeviceCreateSwapChain(RawId selfId, RawId queueId, RGBDescriptor desc, RawId[] bufferIds, RemoteTextureOwnerId ownerId);
+ async SwapChainPresent(RawId textureId, RawId commandEncoderId, RemoteTextureId remoteTextureId, RemoteTextureOwnerId remoteTextureOwnerId);
+ async SwapChainDestroy(RemoteTextureOwnerId ownerId);
+
+ async DevicePushErrorScope(RawId selfId);
+ async DevicePopErrorScope(RawId selfId) returns (MaybeScopedError maybeError);
+
+ // Generate an error on the Device timeline for `deviceId`.
+ // The `message` parameter is interpreted as UTF-8.
+ async GenerateError(RawId deviceId, nsCString message);
+
+child:
+ async DeviceUncapturedError(RawId aDeviceId, nsCString message);
+ async DropAction(ByteBuf buf);
+ async __delete__();
+};
+
+} // webgpu
+} // mozilla
diff --git a/dom/webgpu/ipc/PWebGPUTypes.ipdlh b/dom/webgpu/ipc/PWebGPUTypes.ipdlh
new file mode 100644
index 0000000000..98f062856c
--- /dev/null
+++ b/dom/webgpu/ipc/PWebGPUTypes.ipdlh
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+using struct mozilla::null_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace webgpu {
+
+struct BufferMapSuccess {
+ uint64_t offset;
+ uint64_t size;
+ bool writable;
+};
+
+struct BufferMapError {
+ nsCString message;
+};
+
+union BufferMapResult {
+ BufferMapSuccess;
+ BufferMapError;
+};
+
+} // namespace layers
+} // namespace mozilla
diff --git a/dom/webgpu/ipc/WebGPUChild.cpp b/dom/webgpu/ipc/WebGPUChild.cpp
new file mode 100644
index 0000000000..77a9556a2d
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUChild.cpp
@@ -0,0 +1,1249 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebGPUChild.h"
+#include "js/RootingAPI.h"
+#include "js/String.h"
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "js/Warnings.h" // JS::WarnUTF8
+#include "mozilla/Attributes.h"
+#include "mozilla/EnumTypeTraits.h"
+#include "mozilla/dom/Console.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/dom/GPUUncapturedErrorEvent.h"
+#include "mozilla/webgpu/ValidationError.h"
+#include "mozilla/webgpu/WebGPUTypes.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "Adapter.h"
+#include "DeviceLostInfo.h"
+#include "PipelineLayout.h"
+#include "Sampler.h"
+#include "CompilationInfo.h"
+#include "mozilla/ipc/RawShmem.h"
+#include "nsGlobalWindowInner.h"
+
+namespace mozilla::webgpu {
+
+NS_IMPL_CYCLE_COLLECTION(WebGPUChild)
+
+void WebGPUChild::JsWarning(nsIGlobalObject* aGlobal,
+ const nsACString& aMessage) {
+ const auto& flatString = PromiseFlatCString(aMessage);
+ if (aGlobal) {
+ dom::AutoJSAPI api;
+ if (api.Init(aGlobal)) {
+ JS::WarnUTF8(api.cx(), "%s", flatString.get());
+ }
+ } else {
+ printf_stderr("Validation error without device target: %s\n",
+ flatString.get());
+ }
+}
+
+static ffi::WGPUCompareFunction ConvertCompareFunction(
+ const dom::GPUCompareFunction& aCompare) {
+ // Value of 0 = Undefined is reserved on the C side for "null" semantics.
+ return ffi::WGPUCompareFunction(UnderlyingValue(aCompare) + 1);
+}
+
+static ffi::WGPUTextureFormat ConvertTextureFormat(
+ const dom::GPUTextureFormat& aFormat) {
+ ffi::WGPUTextureFormat result = {ffi::WGPUTextureFormat_Sentinel};
+ switch (aFormat) {
+ case dom::GPUTextureFormat::R8unorm:
+ result.tag = ffi::WGPUTextureFormat_R8Unorm;
+ break;
+ case dom::GPUTextureFormat::R8snorm:
+ result.tag = ffi::WGPUTextureFormat_R8Snorm;
+ break;
+ case dom::GPUTextureFormat::R8uint:
+ result.tag = ffi::WGPUTextureFormat_R8Uint;
+ break;
+ case dom::GPUTextureFormat::R8sint:
+ result.tag = ffi::WGPUTextureFormat_R8Sint;
+ break;
+ case dom::GPUTextureFormat::R16uint:
+ result.tag = ffi::WGPUTextureFormat_R16Uint;
+ break;
+ case dom::GPUTextureFormat::R16sint:
+ result.tag = ffi::WGPUTextureFormat_R16Sint;
+ break;
+ case dom::GPUTextureFormat::R16float:
+ result.tag = ffi::WGPUTextureFormat_R16Float;
+ break;
+ case dom::GPUTextureFormat::Rg8unorm:
+ result.tag = ffi::WGPUTextureFormat_Rg8Unorm;
+ break;
+ case dom::GPUTextureFormat::Rg8snorm:
+ result.tag = ffi::WGPUTextureFormat_Rg8Snorm;
+ break;
+ case dom::GPUTextureFormat::Rg8uint:
+ result.tag = ffi::WGPUTextureFormat_Rg8Uint;
+ break;
+ case dom::GPUTextureFormat::Rg8sint:
+ result.tag = ffi::WGPUTextureFormat_Rg8Sint;
+ break;
+ case dom::GPUTextureFormat::R32uint:
+ result.tag = ffi::WGPUTextureFormat_R32Uint;
+ break;
+ case dom::GPUTextureFormat::R32sint:
+ result.tag = ffi::WGPUTextureFormat_R32Sint;
+ break;
+ case dom::GPUTextureFormat::R32float:
+ result.tag = ffi::WGPUTextureFormat_R32Float;
+ break;
+ case dom::GPUTextureFormat::Rg16uint:
+ result.tag = ffi::WGPUTextureFormat_Rg16Uint;
+ break;
+ case dom::GPUTextureFormat::Rg16sint:
+ result.tag = ffi::WGPUTextureFormat_Rg16Sint;
+ break;
+ case dom::GPUTextureFormat::Rg16float:
+ result.tag = ffi::WGPUTextureFormat_Rg16Float;
+ break;
+ case dom::GPUTextureFormat::Rgba8unorm:
+ result.tag = ffi::WGPUTextureFormat_Rgba8Unorm;
+ break;
+ case dom::GPUTextureFormat::Rgba8unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Rgba8UnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Rgba8snorm:
+ result.tag = ffi::WGPUTextureFormat_Rgba8Snorm;
+ break;
+ case dom::GPUTextureFormat::Rgba8uint:
+ result.tag = ffi::WGPUTextureFormat_Rgba8Uint;
+ break;
+ case dom::GPUTextureFormat::Rgba8sint:
+ result.tag = ffi::WGPUTextureFormat_Rgba8Sint;
+ break;
+ case dom::GPUTextureFormat::Bgra8unorm:
+ result.tag = ffi::WGPUTextureFormat_Bgra8Unorm;
+ break;
+ case dom::GPUTextureFormat::Bgra8unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Bgra8UnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Rgb9e5ufloat:
+ result.tag = ffi::WGPUTextureFormat_Rgb9e5Ufloat;
+ break;
+ case dom::GPUTextureFormat::Rgb10a2unorm:
+ result.tag = ffi::WGPUTextureFormat_Rgb10a2Unorm;
+ break;
+ case dom::GPUTextureFormat::Rg11b10float:
+ result.tag = ffi::WGPUTextureFormat_Rg11b10Float;
+ break;
+ case dom::GPUTextureFormat::Rg32uint:
+ result.tag = ffi::WGPUTextureFormat_Rg32Uint;
+ break;
+ case dom::GPUTextureFormat::Rg32sint:
+ result.tag = ffi::WGPUTextureFormat_Rg32Sint;
+ break;
+ case dom::GPUTextureFormat::Rg32float:
+ result.tag = ffi::WGPUTextureFormat_Rg32Float;
+ break;
+ case dom::GPUTextureFormat::Rgba16uint:
+ result.tag = ffi::WGPUTextureFormat_Rgba16Uint;
+ break;
+ case dom::GPUTextureFormat::Rgba16sint:
+ result.tag = ffi::WGPUTextureFormat_Rgba16Sint;
+ break;
+ case dom::GPUTextureFormat::Rgba16float:
+ result.tag = ffi::WGPUTextureFormat_Rgba16Float;
+ break;
+ case dom::GPUTextureFormat::Rgba32uint:
+ result.tag = ffi::WGPUTextureFormat_Rgba32Uint;
+ break;
+ case dom::GPUTextureFormat::Rgba32sint:
+ result.tag = ffi::WGPUTextureFormat_Rgba32Sint;
+ break;
+ case dom::GPUTextureFormat::Rgba32float:
+ result.tag = ffi::WGPUTextureFormat_Rgba32Float;
+ break;
+ case dom::GPUTextureFormat::Depth32float:
+ result.tag = ffi::WGPUTextureFormat_Depth32Float;
+ break;
+ case dom::GPUTextureFormat::Bc1_rgba_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc1RgbaUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc1_rgba_unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Bc1RgbaUnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Bc4_r_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc4RUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc4_r_snorm:
+ result.tag = ffi::WGPUTextureFormat_Bc4RSnorm;
+ break;
+ case dom::GPUTextureFormat::Bc2_rgba_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc2RgbaUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc2_rgba_unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Bc2RgbaUnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Bc3_rgba_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc3RgbaUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc3_rgba_unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Bc3RgbaUnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Bc5_rg_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc5RgUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc5_rg_snorm:
+ result.tag = ffi::WGPUTextureFormat_Bc5RgSnorm;
+ break;
+ case dom::GPUTextureFormat::Bc6h_rgb_ufloat:
+ result.tag = ffi::WGPUTextureFormat_Bc6hRgbUfloat;
+ break;
+ case dom::GPUTextureFormat::Bc6h_rgb_float:
+ result.tag = ffi::WGPUTextureFormat_Bc6hRgbFloat;
+ break;
+ case dom::GPUTextureFormat::Bc7_rgba_unorm:
+ result.tag = ffi::WGPUTextureFormat_Bc7RgbaUnorm;
+ break;
+ case dom::GPUTextureFormat::Bc7_rgba_unorm_srgb:
+ result.tag = ffi::WGPUTextureFormat_Bc7RgbaUnormSrgb;
+ break;
+ case dom::GPUTextureFormat::Depth24plus:
+ result.tag = ffi::WGPUTextureFormat_Depth24Plus;
+ break;
+ case dom::GPUTextureFormat::Depth24plus_stencil8:
+ result.tag = ffi::WGPUTextureFormat_Depth24PlusStencil8;
+ break;
+ case dom::GPUTextureFormat::Depth32float_stencil8:
+ result.tag = ffi::WGPUTextureFormat_Depth32FloatStencil8;
+ break;
+ case dom::GPUTextureFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE();
+ }
+
+ // Clang will check for us that the switch above is exhaustive,
+ // but not if we add a 'default' case. So, check this here.
+ MOZ_ASSERT(result.tag != ffi::WGPUTextureFormat_Sentinel,
+ "unexpected texture format enum");
+
+ return result;
+}
+
+void WebGPUChild::ConvertTextureFormatRef(const dom::GPUTextureFormat& aInput,
+ ffi::WGPUTextureFormat& aOutput) {
+ aOutput = ConvertTextureFormat(aInput);
+}
+
+static UniquePtr<ffi::WGPUClient> initialize() {
+ ffi::WGPUInfrastructure infra = ffi::wgpu_client_new();
+ return UniquePtr<ffi::WGPUClient>{infra.client};
+}
+
+WebGPUChild::WebGPUChild() : mClient(initialize()) {}
+
+WebGPUChild::~WebGPUChild() = default;
+
+RefPtr<AdapterPromise> WebGPUChild::InstanceRequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions) {
+ const int max_ids = 10;
+ RawId ids[max_ids] = {0};
+ unsigned long count =
+ ffi::wgpu_client_make_adapter_ids(mClient.get(), ids, max_ids);
+
+ nsTArray<RawId> sharedIds(count);
+ for (unsigned long i = 0; i != count; ++i) {
+ sharedIds.AppendElement(ids[i]);
+ }
+
+ return SendInstanceRequestAdapter(aOptions, sharedIds)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](ipc::ByteBuf&& aInfoBuf) {
+ // Ideally, we'd just send an empty ByteBuf, but the IPC code
+ // complains if the capacity is zero...
+ // So for the case where an adapter wasn't found, we just
+ // transfer a single 0u64 in this buffer.
+ return aInfoBuf.mLen > sizeof(uint64_t)
+ ? AdapterPromise::CreateAndResolve(std::move(aInfoBuf),
+ __func__)
+ : AdapterPromise::CreateAndReject(Nothing(), __func__);
+ },
+ [](const ipc::ResponseRejectReason& aReason) {
+ return AdapterPromise::CreateAndReject(Some(aReason), __func__);
+ });
+}
+
+Maybe<DeviceRequest> WebGPUChild::AdapterRequestDevice(
+ RawId aSelfId, const dom::GPUDeviceDescriptor& aDesc,
+ ffi::WGPULimits* aLimits) {
+ ffi::WGPUDeviceDescriptor desc = {};
+ ffi::wgpu_client_fill_default_limits(&desc.limits);
+
+ // webgpu::StringHelper label(aDesc.mLabel);
+ // desc.label = label.Get();
+
+ const auto featureBits = Adapter::MakeFeatureBits(aDesc.mRequiredFeatures);
+ if (!featureBits) {
+ return Nothing();
+ }
+ desc.features = *featureBits;
+
+ if (aDesc.mRequiredLimits.WasPassed()) {
+ for (const auto& entry : aDesc.mRequiredLimits.Value().Entries()) {
+ const uint32_t valueU32 =
+ entry.mValue < std::numeric_limits<uint32_t>::max()
+ ? entry.mValue
+ : std::numeric_limits<uint32_t>::max();
+ if (entry.mKey == u"maxTextureDimension1D"_ns) {
+ desc.limits.max_texture_dimension_1d = valueU32;
+ } else if (entry.mKey == u"maxTextureDimension2D"_ns) {
+ desc.limits.max_texture_dimension_2d = valueU32;
+ } else if (entry.mKey == u"maxTextureDimension3D"_ns) {
+ desc.limits.max_texture_dimension_3d = valueU32;
+ } else if (entry.mKey == u"maxTextureArrayLayers"_ns) {
+ desc.limits.max_texture_array_layers = valueU32;
+ } else if (entry.mKey == u"maxBindGroups"_ns) {
+ desc.limits.max_bind_groups = valueU32;
+ } else if (entry.mKey ==
+ u"maxDynamicUniformBuffersPerPipelineLayout"_ns) {
+ desc.limits.max_dynamic_uniform_buffers_per_pipeline_layout = valueU32;
+ } else if (entry.mKey ==
+ u"maxDynamicStorageBuffersPerPipelineLayout"_ns) {
+ desc.limits.max_dynamic_storage_buffers_per_pipeline_layout = valueU32;
+ } else if (entry.mKey == u"maxSampledTexturesPerShaderStage"_ns) {
+ desc.limits.max_sampled_textures_per_shader_stage = valueU32;
+ } else if (entry.mKey == u"maxSamplersPerShaderStage"_ns) {
+ desc.limits.max_samplers_per_shader_stage = valueU32;
+ } else if (entry.mKey == u"maxStorageBuffersPerShaderStage"_ns) {
+ desc.limits.max_storage_buffers_per_shader_stage = valueU32;
+ } else if (entry.mKey == u"maxStorageTexturesPerShaderStage"_ns) {
+ desc.limits.max_storage_textures_per_shader_stage = valueU32;
+ } else if (entry.mKey == u"maxUniformBuffersPerShaderStage"_ns) {
+ desc.limits.max_uniform_buffers_per_shader_stage = valueU32;
+ } else if (entry.mKey == u"maxUniformBufferBindingSize"_ns) {
+ desc.limits.max_uniform_buffer_binding_size = entry.mValue;
+ } else if (entry.mKey == u"maxStorageBufferBindingSize"_ns) {
+ desc.limits.max_storage_buffer_binding_size = entry.mValue;
+ } else if (entry.mKey == u"minUniformBufferOffsetAlignment"_ns) {
+ desc.limits.min_uniform_buffer_offset_alignment = valueU32;
+ } else if (entry.mKey == u"minStorageBufferOffsetAlignment"_ns) {
+ desc.limits.min_storage_buffer_offset_alignment = valueU32;
+ } else if (entry.mKey == u"maxVertexBuffers"_ns) {
+ desc.limits.max_vertex_buffers = valueU32;
+ } else if (entry.mKey == u"maxVertexAttributes"_ns) {
+ desc.limits.max_vertex_attributes = valueU32;
+ } else if (entry.mKey == u"maxVertexBufferArrayStride"_ns) {
+ desc.limits.max_vertex_buffer_array_stride = valueU32;
+ } else if (entry.mKey == u"maxComputeWorkgroupSizeX"_ns) {
+ desc.limits.max_compute_workgroup_size_x = valueU32;
+ } else if (entry.mKey == u"maxComputeWorkgroupSizeY"_ns) {
+ desc.limits.max_compute_workgroup_size_y = valueU32;
+ } else if (entry.mKey == u"maxComputeWorkgroupSizeZ"_ns) {
+ desc.limits.max_compute_workgroup_size_z = valueU32;
+ } else if (entry.mKey == u"maxComputeWorkgroupsPerDimension"_ns) {
+ desc.limits.max_compute_workgroups_per_dimension = valueU32;
+ } else {
+ NS_WARNING(nsPrintfCString("Requested limit '%s' is not recognized.",
+ NS_ConvertUTF16toUTF8(entry.mKey).get())
+ .get());
+ return Nothing();
+ }
+
+ // TODO: maxInterStageShaderComponents
+ // TODO: maxComputeWorkgroupStorageSize
+ // TODO: maxComputeInvocationsPerWorkgroup
+ }
+ }
+
+ RawId id = ffi::wgpu_client_make_device_id(mClient.get(), aSelfId);
+
+ ByteBuf bb;
+ ffi::wgpu_client_serialize_device_descriptor(&desc, ToFFI(&bb));
+
+ DeviceRequest request;
+ request.mId = id;
+ request.mPromise = SendAdapterRequestDevice(aSelfId, std::move(bb), id);
+ *aLimits = desc.limits;
+
+ return Some(std::move(request));
+}
+
+RawId WebGPUChild::DeviceCreateBuffer(RawId aSelfId,
+ const dom::GPUBufferDescriptor& aDesc,
+ ipc::UnsafeSharedMemoryHandle&& aShmem) {
+ RawId bufferId = ffi::wgpu_client_make_buffer_id(mClient.get(), aSelfId);
+ if (!SendCreateBuffer(aSelfId, bufferId, aDesc, std::move(aShmem))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return bufferId;
+}
+
+RawId WebGPUChild::DeviceCreateTexture(RawId aSelfId,
+ const dom::GPUTextureDescriptor& aDesc) {
+ ffi::WGPUTextureDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ // TODO: bug 1773723
+ desc.view_formats = {nullptr, 0};
+
+ if (aDesc.mSize.IsRangeEnforcedUnsignedLongSequence()) {
+ const auto& seq = aDesc.mSize.GetAsRangeEnforcedUnsignedLongSequence();
+ desc.size.width = seq.Length() > 0 ? seq[0] : 1;
+ desc.size.height = seq.Length() > 1 ? seq[1] : 1;
+ desc.size.depth_or_array_layers = seq.Length() > 2 ? seq[2] : 1;
+ } else if (aDesc.mSize.IsGPUExtent3DDict()) {
+ const auto& dict = aDesc.mSize.GetAsGPUExtent3DDict();
+ desc.size.width = dict.mWidth;
+ desc.size.height = dict.mHeight;
+ desc.size.depth_or_array_layers = dict.mDepthOrArrayLayers;
+ } else {
+ MOZ_CRASH("Unexpected union");
+ }
+ desc.mip_level_count = aDesc.mMipLevelCount;
+ desc.sample_count = aDesc.mSampleCount;
+ desc.dimension = ffi::WGPUTextureDimension(aDesc.mDimension);
+ desc.format = ConvertTextureFormat(aDesc.mFormat);
+ desc.usage = aDesc.mUsage;
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_texture(mClient.get(), aSelfId, &desc,
+ ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::TextureCreateView(
+ RawId aSelfId, RawId aDeviceId,
+ const dom::GPUTextureViewDescriptor& aDesc) {
+ ffi::WGPUTextureViewDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ffi::WGPUTextureFormat format = {ffi::WGPUTextureFormat_Sentinel};
+ if (aDesc.mFormat.WasPassed()) {
+ format = ConvertTextureFormat(aDesc.mFormat.Value());
+ desc.format = &format;
+ }
+ ffi::WGPUTextureViewDimension dimension =
+ ffi::WGPUTextureViewDimension_Sentinel;
+ if (aDesc.mDimension.WasPassed()) {
+ dimension = ffi::WGPUTextureViewDimension(aDesc.mDimension.Value());
+ desc.dimension = &dimension;
+ }
+
+ // Ideally we'd just do something like "aDesc.mMipLevelCount.ptrOr(nullptr)"
+ // but dom::Optional does not seem to have very many nice things.
+ uint32_t mipCount =
+ aDesc.mMipLevelCount.WasPassed() ? aDesc.mMipLevelCount.Value() : 0;
+ uint32_t layerCount =
+ aDesc.mArrayLayerCount.WasPassed() ? aDesc.mArrayLayerCount.Value() : 0;
+
+ desc.aspect = ffi::WGPUTextureAspect(aDesc.mAspect);
+ desc.base_mip_level = aDesc.mBaseMipLevel;
+ desc.mip_level_count = aDesc.mMipLevelCount.WasPassed() ? &mipCount : nullptr;
+ desc.base_array_layer = aDesc.mBaseArrayLayer;
+ desc.array_layer_count =
+ aDesc.mArrayLayerCount.WasPassed() ? &layerCount : nullptr;
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_texture_view(mClient.get(), aSelfId, &desc,
+ ToFFI(&bb));
+ if (!SendTextureAction(aSelfId, aDeviceId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateSampler(RawId aSelfId,
+ const dom::GPUSamplerDescriptor& aDesc) {
+ ffi::WGPUSamplerDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+ desc.address_modes[0] = ffi::WGPUAddressMode(aDesc.mAddressModeU);
+ desc.address_modes[1] = ffi::WGPUAddressMode(aDesc.mAddressModeV);
+ desc.address_modes[2] = ffi::WGPUAddressMode(aDesc.mAddressModeW);
+ desc.mag_filter = ffi::WGPUFilterMode(aDesc.mMagFilter);
+ desc.min_filter = ffi::WGPUFilterMode(aDesc.mMinFilter);
+ desc.mipmap_filter = ffi::WGPUFilterMode(aDesc.mMipmapFilter);
+ desc.lod_min_clamp = aDesc.mLodMinClamp;
+ desc.lod_max_clamp = aDesc.mLodMaxClamp;
+
+ ffi::WGPUCompareFunction comparison = ffi::WGPUCompareFunction_Sentinel;
+ if (aDesc.mCompare.WasPassed()) {
+ comparison = ConvertCompareFunction(aDesc.mCompare.Value());
+ desc.compare = &comparison;
+ }
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_sampler(mClient.get(), aSelfId, &desc,
+ ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateCommandEncoder(
+ RawId aSelfId, const dom::GPUCommandEncoderDescriptor& aDesc) {
+ ffi::WGPUCommandEncoderDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_command_encoder(mClient.get(), aSelfId,
+ &desc, ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::CommandEncoderFinish(
+ RawId aSelfId, RawId aDeviceId,
+ const dom::GPUCommandBufferDescriptor& aDesc) {
+ if (!SendCommandEncoderFinish(aSelfId, aDeviceId, aDesc)) {
+ MOZ_CRASH("IPC failure");
+ }
+ // We rely on knowledge that `CommandEncoderId` == `CommandBufferId`
+ // TODO: refactor this to truly behave as if the encoder is being finished,
+ // and a new command buffer ID is being created from it. Resolve the ID
+ // type aliasing at the place that introduces it: `wgpu-core`.
+ return aSelfId;
+}
+
+RawId WebGPUChild::RenderBundleEncoderFinish(
+ ffi::WGPURenderBundleEncoder& aEncoder, RawId aDeviceId,
+ const dom::GPURenderBundleDescriptor& aDesc) {
+ ffi::WGPURenderBundleDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ipc::ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_render_bundle(
+ mClient.get(), &aEncoder, aDeviceId, &desc, ToFFI(&bb));
+
+ if (!SendDeviceAction(aDeviceId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateBindGroupLayout(
+ RawId aSelfId, const dom::GPUBindGroupLayoutDescriptor& aDesc) {
+ struct OptionalData {
+ ffi::WGPUTextureViewDimension dim;
+ ffi::WGPURawTextureSampleType type;
+ ffi::WGPUTextureFormat format;
+ };
+ nsTArray<OptionalData> optional(aDesc.mEntries.Length());
+ for (const auto& entry : aDesc.mEntries) {
+ OptionalData data = {};
+ if (entry.mTexture.WasPassed()) {
+ const auto& texture = entry.mTexture.Value();
+ data.dim = ffi::WGPUTextureViewDimension(texture.mViewDimension);
+ switch (texture.mSampleType) {
+ case dom::GPUTextureSampleType::Float:
+ data.type = ffi::WGPURawTextureSampleType_Float;
+ break;
+ case dom::GPUTextureSampleType::Unfilterable_float:
+ data.type = ffi::WGPURawTextureSampleType_UnfilterableFloat;
+ break;
+ case dom::GPUTextureSampleType::Uint:
+ data.type = ffi::WGPURawTextureSampleType_Uint;
+ break;
+ case dom::GPUTextureSampleType::Sint:
+ data.type = ffi::WGPURawTextureSampleType_Sint;
+ break;
+ case dom::GPUTextureSampleType::Depth:
+ data.type = ffi::WGPURawTextureSampleType_Depth;
+ break;
+ case dom::GPUTextureSampleType::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE();
+ }
+ }
+ if (entry.mStorageTexture.WasPassed()) {
+ const auto& texture = entry.mStorageTexture.Value();
+ data.dim = ffi::WGPUTextureViewDimension(texture.mViewDimension);
+ data.format = ConvertTextureFormat(texture.mFormat);
+ }
+ optional.AppendElement(data);
+ }
+
+ nsTArray<ffi::WGPUBindGroupLayoutEntry> entries(aDesc.mEntries.Length());
+ for (size_t i = 0; i < aDesc.mEntries.Length(); ++i) {
+ const auto& entry = aDesc.mEntries[i];
+ ffi::WGPUBindGroupLayoutEntry e = {};
+ e.binding = entry.mBinding;
+ e.visibility = entry.mVisibility;
+ if (entry.mBuffer.WasPassed()) {
+ switch (entry.mBuffer.Value().mType) {
+ case dom::GPUBufferBindingType::Uniform:
+ e.ty = ffi::WGPURawBindingType_UniformBuffer;
+ break;
+ case dom::GPUBufferBindingType::Storage:
+ e.ty = ffi::WGPURawBindingType_StorageBuffer;
+ break;
+ case dom::GPUBufferBindingType::Read_only_storage:
+ e.ty = ffi::WGPURawBindingType_ReadonlyStorageBuffer;
+ break;
+ case dom::GPUBufferBindingType::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE();
+ }
+ e.has_dynamic_offset = entry.mBuffer.Value().mHasDynamicOffset;
+ }
+ if (entry.mTexture.WasPassed()) {
+ e.ty = ffi::WGPURawBindingType_SampledTexture;
+ e.view_dimension = &optional[i].dim;
+ e.texture_sample_type = &optional[i].type;
+ e.multisampled = entry.mTexture.Value().mMultisampled;
+ }
+ if (entry.mStorageTexture.WasPassed()) {
+ e.ty = entry.mStorageTexture.Value().mAccess ==
+ dom::GPUStorageTextureAccess::Write_only
+ ? ffi::WGPURawBindingType_WriteonlyStorageTexture
+ : ffi::WGPURawBindingType_ReadonlyStorageTexture;
+ e.view_dimension = &optional[i].dim;
+ e.storage_texture_format = &optional[i].format;
+ }
+ if (entry.mSampler.WasPassed()) {
+ e.ty = ffi::WGPURawBindingType_Sampler;
+ switch (entry.mSampler.Value().mType) {
+ case dom::GPUSamplerBindingType::Filtering:
+ e.sampler_filter = true;
+ break;
+ case dom::GPUSamplerBindingType::Non_filtering:
+ break;
+ case dom::GPUSamplerBindingType::Comparison:
+ e.sampler_compare = true;
+ break;
+ case dom::GPUSamplerBindingType::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE();
+ }
+ }
+ entries.AppendElement(e);
+ }
+
+ ffi::WGPUBindGroupLayoutDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+ desc.entries = entries.Elements();
+ desc.entries_length = entries.Length();
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_bind_group_layout(mClient.get(), aSelfId,
+ &desc, ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreatePipelineLayout(
+ RawId aSelfId, const dom::GPUPipelineLayoutDescriptor& aDesc) {
+ nsTArray<ffi::WGPUBindGroupLayoutId> bindGroupLayouts(
+ aDesc.mBindGroupLayouts.Length());
+ for (const auto& layout : aDesc.mBindGroupLayouts) {
+ if (!layout->IsValid()) {
+ return 0;
+ }
+ bindGroupLayouts.AppendElement(layout->mId);
+ }
+
+ ffi::WGPUPipelineLayoutDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+ desc.bind_group_layouts = bindGroupLayouts.Elements();
+ desc.bind_group_layouts_length = bindGroupLayouts.Length();
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_pipeline_layout(mClient.get(), aSelfId,
+ &desc, ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateBindGroup(
+ RawId aSelfId, const dom::GPUBindGroupDescriptor& aDesc) {
+ if (!aDesc.mLayout->IsValid()) {
+ return 0;
+ }
+
+ nsTArray<ffi::WGPUBindGroupEntry> entries(aDesc.mEntries.Length());
+ for (const auto& entry : aDesc.mEntries) {
+ ffi::WGPUBindGroupEntry e = {};
+ e.binding = entry.mBinding;
+ if (entry.mResource.IsGPUBufferBinding()) {
+ const auto& bufBinding = entry.mResource.GetAsGPUBufferBinding();
+ e.buffer = bufBinding.mBuffer->mId;
+ e.offset = bufBinding.mOffset;
+ e.size = bufBinding.mSize.WasPassed() ? bufBinding.mSize.Value() : 0;
+ }
+ if (entry.mResource.IsGPUTextureView()) {
+ e.texture_view = entry.mResource.GetAsGPUTextureView()->mId;
+ }
+ if (entry.mResource.IsGPUSampler()) {
+ e.sampler = entry.mResource.GetAsGPUSampler()->mId;
+ }
+ entries.AppendElement(e);
+ }
+
+ ffi::WGPUBindGroupDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+ desc.layout = aDesc.mLayout->mId;
+ desc.entries = entries.Elements();
+ desc.entries_length = entries.Length();
+
+ ByteBuf bb;
+ RawId id = ffi::wgpu_client_create_bind_group(mClient.get(), aSelfId, &desc,
+ ToFFI(&bb));
+ if (!SendDeviceAction(aSelfId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+MOZ_CAN_RUN_SCRIPT void reportCompilationMessagesToConsole(
+ const RefPtr<ShaderModule>& aShaderModule,
+ const nsTArray<WebGPUCompilationMessage>& aMessages) {
+ auto* global = aShaderModule->GetParentObject();
+
+ dom::AutoJSAPI api;
+ if (!api.Init(global)) {
+ return;
+ }
+
+ const auto& cx = api.cx();
+
+ ErrorResult rv;
+ RefPtr<dom::Console> console =
+ nsGlobalWindowInner::Cast(global->AsInnerWindow())->GetConsole(cx, rv);
+ if (rv.Failed()) {
+ return;
+ }
+
+ dom::GlobalObject globalObj(cx, global->GetGlobalJSObject());
+
+ dom::Sequence<JS::Value> args;
+ dom::SequenceRooter<JS::Value> msgArgsRooter(cx, &args);
+ auto SetSingleStrAsArgs =
+ [&](const nsString& message, dom::Sequence<JS::Value>* args)
+ MOZ_CAN_RUN_SCRIPT {
+ args->Clear();
+ JS::Rooted<JSString*> jsStr(
+ cx, JS_NewUCStringCopyN(cx, message.Data(), message.Length()));
+ if (!jsStr) {
+ return;
+ }
+ JS::Rooted<JS::Value> val(cx, JS::StringValue(jsStr));
+ if (!args->AppendElement(val, fallible)) {
+ return;
+ }
+ };
+
+ nsString label;
+ aShaderModule->GetLabel(label);
+ auto appendNiceLabelIfPresent = [&label](nsString* buf) MOZ_CAN_RUN_SCRIPT {
+ if (!label.IsEmpty()) {
+ buf->AppendLiteral(u" \"");
+ buf->Append(label);
+ buf->AppendLiteral(u"\"");
+ }
+ };
+
+ // We haven't actually inspected a message for severity, but
+ // it doesn't actually matter, since we don't do anything at
+ // this level.
+ auto highestSeveritySeen = WebGPUCompilationMessageType::Info;
+ uint64_t errorCount = 0;
+ uint64_t warningCount = 0;
+ uint64_t infoCount = 0;
+ for (const auto& message : aMessages) {
+ bool higherThanSeen =
+ static_cast<std::underlying_type_t<WebGPUCompilationMessageType>>(
+ message.messageType) <
+ static_cast<std::underlying_type_t<WebGPUCompilationMessageType>>(
+ highestSeveritySeen);
+ if (higherThanSeen) {
+ highestSeveritySeen = message.messageType;
+ }
+ switch (message.messageType) {
+ case WebGPUCompilationMessageType::Error:
+ errorCount += 1;
+ break;
+ case WebGPUCompilationMessageType::Warning:
+ warningCount += 1;
+ break;
+ case WebGPUCompilationMessageType::Info:
+ infoCount += 1;
+ break;
+ }
+ }
+ switch (highestSeveritySeen) {
+ case WebGPUCompilationMessageType::Info:
+ // shouldn't happen, but :shrug:
+ break;
+ case WebGPUCompilationMessageType::Warning: {
+ nsString msg(
+ u"Encountered one or more warnings while creating shader module");
+ appendNiceLabelIfPresent(&msg);
+ SetSingleStrAsArgs(msg, &args);
+ console->Warn(globalObj, args);
+ break;
+ }
+ case WebGPUCompilationMessageType::Error: {
+ nsString msg(
+ u"Encountered one or more errors while creating shader module");
+ appendNiceLabelIfPresent(&msg);
+ SetSingleStrAsArgs(msg, &args);
+ console->Error(globalObj, args);
+ break;
+ }
+ }
+
+ nsString header;
+ header.AppendLiteral(u"WebGPU compilation info for shader module");
+ appendNiceLabelIfPresent(&header);
+ header.AppendLiteral(u" (");
+ header.AppendInt(errorCount);
+ header.AppendLiteral(u" error(s), ");
+ header.AppendInt(warningCount);
+ header.AppendLiteral(u" warning(s), ");
+ header.AppendInt(infoCount);
+ header.AppendLiteral(u" info)");
+ SetSingleStrAsArgs(header, &args);
+ console->GroupCollapsed(globalObj, args);
+
+ for (const auto& message : aMessages) {
+ SetSingleStrAsArgs(message.message, &args);
+ switch (message.messageType) {
+ case WebGPUCompilationMessageType::Error:
+ console->Error(globalObj, args);
+ break;
+ case WebGPUCompilationMessageType::Warning:
+ console->Warn(globalObj, args);
+ break;
+ case WebGPUCompilationMessageType::Info:
+ console->Info(globalObj, args);
+ break;
+ }
+ }
+ console->GroupEnd(globalObj);
+}
+
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION already_AddRefed<ShaderModule>
+WebGPUChild::DeviceCreateShaderModule(
+ Device& aDevice, const dom::GPUShaderModuleDescriptor& aDesc,
+ RefPtr<dom::Promise> aPromise) {
+ RawId deviceId = aDevice.mId;
+ RawId moduleId =
+ ffi::wgpu_client_make_shader_module_id(mClient.get(), deviceId);
+
+ RefPtr<ShaderModule> shaderModule =
+ new ShaderModule(&aDevice, moduleId, aPromise);
+
+ nsString noLabel;
+ nsString& label = noLabel;
+ if (aDesc.mLabel.WasPassed()) {
+ label = aDesc.mLabel.Value();
+ shaderModule->SetLabel(label);
+ }
+ SendDeviceCreateShaderModule(deviceId, moduleId, label, aDesc.mCode)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aPromise,
+ shaderModule](nsTArray<WebGPUCompilationMessage>&& messages)
+ MOZ_CAN_RUN_SCRIPT {
+ if (!messages.IsEmpty()) {
+ reportCompilationMessagesToConsole(shaderModule,
+ std::cref(messages));
+ }
+ RefPtr<CompilationInfo> infoObject(
+ new CompilationInfo(shaderModule));
+ infoObject->SetMessages(messages);
+ aPromise->MaybeResolve(infoObject);
+ },
+ [aPromise](const ipc::ResponseRejectReason& aReason) {
+ aPromise->MaybeRejectWithNotSupportedError("IPC error");
+ });
+
+ return shaderModule.forget();
+}
+
+RawId WebGPUChild::DeviceCreateComputePipelineImpl(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc, ByteBuf* const aByteBuf) {
+ ffi::WGPUComputePipelineDescriptor desc = {};
+ nsCString label, entryPoint;
+ if (aDesc.mLabel.WasPassed()) {
+ CopyUTF16toUTF8(aDesc.mLabel.Value(), label);
+ desc.label = label.get();
+ }
+ if (aDesc.mLayout.IsGPUAutoLayoutMode()) {
+ desc.layout = 0;
+ } else if (aDesc.mLayout.IsGPUPipelineLayout()) {
+ desc.layout = aDesc.mLayout.GetAsGPUPipelineLayout()->mId;
+ } else {
+ MOZ_ASSERT_UNREACHABLE();
+ }
+ desc.stage.module = aDesc.mCompute.mModule->mId;
+ CopyUTF16toUTF8(aDesc.mCompute.mEntryPoint, entryPoint);
+ desc.stage.entry_point = entryPoint.get();
+
+ RawId implicit_bgl_ids[WGPUMAX_BIND_GROUPS] = {};
+ RawId id = ffi::wgpu_client_create_compute_pipeline(
+ mClient.get(), aContext->mParentId, &desc, ToFFI(aByteBuf),
+ &aContext->mImplicitPipelineLayoutId, implicit_bgl_ids);
+
+ for (const auto& cur : implicit_bgl_ids) {
+ if (!cur) break;
+ aContext->mImplicitBindGroupLayoutIds.AppendElement(cur);
+ }
+
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateComputePipeline(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc) {
+ ByteBuf bb;
+ const RawId id = DeviceCreateComputePipelineImpl(aContext, aDesc, &bb);
+
+ if (!SendDeviceAction(aContext->mParentId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RefPtr<PipelinePromise> WebGPUChild::DeviceCreateComputePipelineAsync(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc) {
+ ByteBuf bb;
+ const RawId id = DeviceCreateComputePipelineImpl(aContext, aDesc, &bb);
+
+ return SendDeviceActionWithAck(aContext->mParentId, std::move(bb))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [id](bool aDummy) {
+ Unused << aDummy;
+ return PipelinePromise::CreateAndResolve(id, __func__);
+ },
+ [](const ipc::ResponseRejectReason& aReason) {
+ return PipelinePromise::CreateAndReject(aReason, __func__);
+ });
+}
+
+static ffi::WGPUMultisampleState ConvertMultisampleState(
+ const dom::GPUMultisampleState& aDesc) {
+ ffi::WGPUMultisampleState desc = {};
+ desc.count = aDesc.mCount;
+ desc.mask = aDesc.mMask;
+ desc.alpha_to_coverage_enabled = aDesc.mAlphaToCoverageEnabled;
+ return desc;
+}
+
+static ffi::WGPUBlendComponent ConvertBlendComponent(
+ const dom::GPUBlendComponent& aDesc) {
+ ffi::WGPUBlendComponent desc = {};
+ desc.src_factor = ffi::WGPUBlendFactor(aDesc.mSrcFactor);
+ desc.dst_factor = ffi::WGPUBlendFactor(aDesc.mDstFactor);
+ desc.operation = ffi::WGPUBlendOperation(aDesc.mOperation);
+ return desc;
+}
+
+static ffi::WGPUStencilFaceState ConvertStencilFaceState(
+ const dom::GPUStencilFaceState& aDesc) {
+ ffi::WGPUStencilFaceState desc = {};
+ desc.compare = ConvertCompareFunction(aDesc.mCompare);
+ desc.fail_op = ffi::WGPUStencilOperation(aDesc.mFailOp);
+ desc.depth_fail_op = ffi::WGPUStencilOperation(aDesc.mDepthFailOp);
+ desc.pass_op = ffi::WGPUStencilOperation(aDesc.mPassOp);
+ return desc;
+}
+
+static ffi::WGPUDepthStencilState ConvertDepthStencilState(
+ const dom::GPUDepthStencilState& aDesc) {
+ ffi::WGPUDepthStencilState desc = {};
+ desc.format = ConvertTextureFormat(aDesc.mFormat);
+ desc.depth_write_enabled = aDesc.mDepthWriteEnabled;
+ desc.depth_compare = ConvertCompareFunction(aDesc.mDepthCompare);
+ desc.stencil.front = ConvertStencilFaceState(aDesc.mStencilFront);
+ desc.stencil.back = ConvertStencilFaceState(aDesc.mStencilBack);
+ desc.stencil.read_mask = aDesc.mStencilReadMask;
+ desc.stencil.write_mask = aDesc.mStencilWriteMask;
+ desc.bias.constant = aDesc.mDepthBias;
+ desc.bias.slope_scale = aDesc.mDepthBiasSlopeScale;
+ desc.bias.clamp = aDesc.mDepthBiasClamp;
+ return desc;
+}
+
+RawId WebGPUChild::DeviceCreateRenderPipelineImpl(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc, ByteBuf* const aByteBuf) {
+ // A bunch of stack locals that we can have pointers into
+ nsTArray<ffi::WGPUVertexBufferLayout> vertexBuffers;
+ nsTArray<ffi::WGPUVertexAttribute> vertexAttributes;
+ ffi::WGPURenderPipelineDescriptor desc = {};
+ nsCString vsEntry, fsEntry;
+ ffi::WGPUIndexFormat stripIndexFormat = ffi::WGPUIndexFormat_Uint16;
+ ffi::WGPUFace cullFace = ffi::WGPUFace_Front;
+ ffi::WGPUVertexState vertexState = {};
+ ffi::WGPUFragmentState fragmentState = {};
+ nsTArray<ffi::WGPUColorTargetState> colorStates;
+ nsTArray<ffi::WGPUBlendState> blendStates;
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ if (aDesc.mLayout.IsGPUAutoLayoutMode()) {
+ desc.layout = 0;
+ } else if (aDesc.mLayout.IsGPUPipelineLayout()) {
+ desc.layout = aDesc.mLayout.GetAsGPUPipelineLayout()->mId;
+ } else {
+ MOZ_ASSERT_UNREACHABLE();
+ }
+
+ {
+ const auto& stage = aDesc.mVertex;
+ vertexState.stage.module = stage.mModule->mId;
+ CopyUTF16toUTF8(stage.mEntryPoint, vsEntry);
+ vertexState.stage.entry_point = vsEntry.get();
+
+ for (const auto& vertex_desc : stage.mBuffers) {
+ ffi::WGPUVertexBufferLayout vb_desc = {};
+ if (!vertex_desc.IsNull()) {
+ const auto& vd = vertex_desc.Value();
+ vb_desc.array_stride = vd.mArrayStride;
+ vb_desc.step_mode = ffi::WGPUVertexStepMode(vd.mStepMode);
+ // Note: we are setting the length but not the pointer
+ vb_desc.attributes_length = vd.mAttributes.Length();
+ for (const auto& vat : vd.mAttributes) {
+ ffi::WGPUVertexAttribute ad = {};
+ ad.offset = vat.mOffset;
+ ad.format = ffi::WGPUVertexFormat(vat.mFormat);
+ ad.shader_location = vat.mShaderLocation;
+ vertexAttributes.AppendElement(ad);
+ }
+ }
+ vertexBuffers.AppendElement(vb_desc);
+ }
+ // Now patch up all the pointers to attribute lists.
+ size_t numAttributes = 0;
+ for (auto& vb_desc : vertexBuffers) {
+ vb_desc.attributes = vertexAttributes.Elements() + numAttributes;
+ numAttributes += vb_desc.attributes_length;
+ }
+
+ vertexState.buffers = vertexBuffers.Elements();
+ vertexState.buffers_length = vertexBuffers.Length();
+ desc.vertex = &vertexState;
+ }
+
+ if (aDesc.mFragment.WasPassed()) {
+ const auto& stage = aDesc.mFragment.Value();
+ fragmentState.stage.module = stage.mModule->mId;
+ CopyUTF16toUTF8(stage.mEntryPoint, fsEntry);
+ fragmentState.stage.entry_point = fsEntry.get();
+
+ // Note: we pre-collect the blend states into a different array
+ // so that we can have non-stale pointers into it.
+ for (const auto& colorState : stage.mTargets) {
+ ffi::WGPUColorTargetState desc = {};
+ desc.format = ConvertTextureFormat(colorState.mFormat);
+ desc.write_mask = colorState.mWriteMask;
+ colorStates.AppendElement(desc);
+ ffi::WGPUBlendState bs = {};
+ if (colorState.mBlend.WasPassed()) {
+ const auto& blend = colorState.mBlend.Value();
+ bs.alpha = ConvertBlendComponent(blend.mAlpha);
+ bs.color = ConvertBlendComponent(blend.mColor);
+ }
+ blendStates.AppendElement(bs);
+ }
+ for (size_t i = 0; i < colorStates.Length(); ++i) {
+ if (stage.mTargets[i].mBlend.WasPassed()) {
+ colorStates[i].blend = &blendStates[i];
+ }
+ }
+
+ fragmentState.targets = colorStates.Elements();
+ fragmentState.targets_length = colorStates.Length();
+ desc.fragment = &fragmentState;
+ }
+
+ {
+ const auto& prim = aDesc.mPrimitive;
+ desc.primitive.topology = ffi::WGPUPrimitiveTopology(prim.mTopology);
+ if (prim.mStripIndexFormat.WasPassed()) {
+ stripIndexFormat = ffi::WGPUIndexFormat(prim.mStripIndexFormat.Value());
+ desc.primitive.strip_index_format = &stripIndexFormat;
+ }
+ desc.primitive.front_face = ffi::WGPUFrontFace(prim.mFrontFace);
+ if (prim.mCullMode != dom::GPUCullMode::None) {
+ cullFace = prim.mCullMode == dom::GPUCullMode::Front ? ffi::WGPUFace_Front
+ : ffi::WGPUFace_Back;
+ desc.primitive.cull_mode = &cullFace;
+ }
+ }
+ desc.multisample = ConvertMultisampleState(aDesc.mMultisample);
+
+ ffi::WGPUDepthStencilState depthStencilState = {};
+ if (aDesc.mDepthStencil.WasPassed()) {
+ depthStencilState = ConvertDepthStencilState(aDesc.mDepthStencil.Value());
+ desc.depth_stencil = &depthStencilState;
+ }
+
+ RawId implicit_bgl_ids[WGPUMAX_BIND_GROUPS] = {};
+ RawId id = ffi::wgpu_client_create_render_pipeline(
+ mClient.get(), aContext->mParentId, &desc, ToFFI(aByteBuf),
+ &aContext->mImplicitPipelineLayoutId, implicit_bgl_ids);
+
+ for (const auto& cur : implicit_bgl_ids) {
+ if (!cur) break;
+ aContext->mImplicitBindGroupLayoutIds.AppendElement(cur);
+ }
+
+ return id;
+}
+
+RawId WebGPUChild::DeviceCreateRenderPipeline(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc) {
+ ByteBuf bb;
+ const RawId id = DeviceCreateRenderPipelineImpl(aContext, aDesc, &bb);
+
+ if (!SendDeviceAction(aContext->mParentId, std::move(bb))) {
+ MOZ_CRASH("IPC failure");
+ }
+ return id;
+}
+
+RefPtr<PipelinePromise> WebGPUChild::DeviceCreateRenderPipelineAsync(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc) {
+ ByteBuf bb;
+ const RawId id = DeviceCreateRenderPipelineImpl(aContext, aDesc, &bb);
+
+ return SendDeviceActionWithAck(aContext->mParentId, std::move(bb))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [id](bool aDummy) {
+ Unused << aDummy;
+ return PipelinePromise::CreateAndResolve(id, __func__);
+ },
+ [](const ipc::ResponseRejectReason& aReason) {
+ return PipelinePromise::CreateAndReject(aReason, __func__);
+ });
+}
+
+ipc::IPCResult WebGPUChild::RecvDeviceUncapturedError(
+ RawId aDeviceId, const nsACString& aMessage) {
+ auto targetIter = mDeviceMap.find(aDeviceId);
+ if (!aDeviceId || targetIter == mDeviceMap.end()) {
+ JsWarning(nullptr, aMessage);
+ } else {
+ auto* target = targetIter->second.get();
+ MOZ_ASSERT(target);
+ // We don't want to spam the errors to the console indefinitely
+ if (target->CheckNewWarning(aMessage)) {
+ JsWarning(target->GetOwnerGlobal(), aMessage);
+
+ dom::GPUUncapturedErrorEventInit init;
+ init.mError.SetAsGPUValidationError() =
+ new ValidationError(target->GetParentObject(), aMessage);
+ RefPtr<mozilla::dom::GPUUncapturedErrorEvent> event =
+ dom::GPUUncapturedErrorEvent::Constructor(
+ target, u"uncapturederror"_ns, init);
+ target->DispatchEvent(*event);
+ }
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUChild::RecvDropAction(const ipc::ByteBuf& aByteBuf) {
+ const auto* byteBuf = ToFFI(&aByteBuf);
+ ffi::wgpu_client_drop_action(mClient.get(), byteBuf);
+ return IPC_OK();
+}
+
+void WebGPUChild::DeviceCreateSwapChain(
+ RawId aSelfId, const RGBDescriptor& aRgbDesc, size_t maxBufferCount,
+ const layers::RemoteTextureOwnerId& aOwnerId) {
+ RawId queueId = aSelfId; // TODO: multiple queues
+ nsTArray<RawId> bufferIds(maxBufferCount);
+ for (size_t i = 0; i < maxBufferCount; ++i) {
+ bufferIds.AppendElement(
+ ffi::wgpu_client_make_buffer_id(mClient.get(), aSelfId));
+ }
+ SendDeviceCreateSwapChain(aSelfId, queueId, aRgbDesc, bufferIds, aOwnerId);
+}
+
+void WebGPUChild::SwapChainPresent(RawId aTextureId,
+ const RemoteTextureId& aRemoteTextureId,
+ const RemoteTextureOwnerId& aOwnerId) {
+ // Hack: the function expects `DeviceId`, but it only uses it for `backend()`
+ // selection.
+ RawId encoderId = ffi::wgpu_client_make_encoder_id(mClient.get(), aTextureId);
+ SendSwapChainPresent(aTextureId, encoderId, aRemoteTextureId, aOwnerId);
+}
+
+void WebGPUChild::RegisterDevice(Device* const aDevice) {
+ mDeviceMap.insert({aDevice->mId, aDevice});
+}
+
+void WebGPUChild::UnregisterDevice(RawId aId) {
+ mDeviceMap.erase(aId);
+ if (IsOpen()) {
+ SendDeviceDestroy(aId);
+ }
+}
+
+void WebGPUChild::FreeUnregisteredInParentDevice(RawId aId) {
+ ffi::wgpu_client_kill_device_id(mClient.get(), aId);
+ mDeviceMap.erase(aId);
+}
+
+void WebGPUChild::ActorDestroy(ActorDestroyReason) {
+ // Resolving the promise could cause us to update the original map if the
+ // callee frees the Device objects immediately. Since any remaining entries
+ // in the map are no longer valid, we can just move the map onto the stack.
+ const auto deviceMap = std::move(mDeviceMap);
+ mDeviceMap.clear();
+
+ for (const auto& targetIter : deviceMap) {
+ RefPtr<Device> device = targetIter.second.get();
+ if (!device) {
+ // The Device may have gotten freed when we resolved the Promise for
+ // another Device in the map.
+ continue;
+ }
+
+ RefPtr<dom::Promise> promise = device->MaybeGetLost();
+ if (!promise) {
+ continue;
+ }
+
+ auto info = MakeRefPtr<DeviceLostInfo>(device->GetParentObject(),
+ u"WebGPUChild destroyed"_ns);
+
+ // We have strong references to both the Device and the DeviceLostInfo and
+ // the Promise objects on the stack which keeps them alive for long enough.
+ promise->MaybeResolve(info);
+ }
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ipc/WebGPUChild.h b/dom/webgpu/ipc/WebGPUChild.h
new file mode 100644
index 0000000000..08f0c6ac77
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUChild.h
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBGPU_CHILD_H_
+#define WEBGPU_CHILD_H_
+
+#include "mozilla/webgpu/PWebGPUChild.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace mozilla {
+namespace ipc {
+class UnsafeSharedMemoryHandle;
+} // namespace ipc
+namespace dom {
+struct GPURequestAdapterOptions;
+} // namespace dom
+namespace layers {
+class CompositorBridgeChild;
+} // namespace layers
+namespace webgpu {
+namespace ffi {
+struct WGPUClient;
+struct WGPULimits;
+struct WGPUTextureViewDescriptor;
+} // namespace ffi
+
+using AdapterPromise =
+ MozPromise<ipc::ByteBuf, Maybe<ipc::ResponseRejectReason>, true>;
+using PipelinePromise = MozPromise<RawId, ipc::ResponseRejectReason, true>;
+using DevicePromise = MozPromise<bool, ipc::ResponseRejectReason, true>;
+
+struct PipelineCreationContext {
+ RawId mParentId = 0;
+ RawId mImplicitPipelineLayoutId = 0;
+ nsTArray<RawId> mImplicitBindGroupLayoutIds;
+};
+
+struct DeviceRequest {
+ RawId mId = 0;
+ RefPtr<DevicePromise> mPromise;
+ // Note: we could put `ffi::WGPULimits` in here as well,
+ // but we don't want to #include ffi stuff in this header
+};
+
+ffi::WGPUByteBuf* ToFFI(ipc::ByteBuf* x);
+
+class WebGPUChild final : public PWebGPUChild, public SupportsWeakPtr {
+ public:
+ friend class layers::CompositorBridgeChild;
+
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(WebGPUChild)
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING_INHERITED(WebGPUChild)
+
+ public:
+ explicit WebGPUChild();
+
+ bool IsOpen() const { return CanSend(); }
+
+ RefPtr<AdapterPromise> InstanceRequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions);
+ Maybe<DeviceRequest> AdapterRequestDevice(
+ RawId aSelfId, const dom::GPUDeviceDescriptor& aDesc,
+ ffi::WGPULimits* aLimits);
+ RawId DeviceCreateBuffer(RawId aSelfId, const dom::GPUBufferDescriptor& aDesc,
+ ipc::UnsafeSharedMemoryHandle&& aShmem);
+ RawId DeviceCreateTexture(RawId aSelfId,
+ const dom::GPUTextureDescriptor& aDesc);
+ RawId TextureCreateView(RawId aSelfId, RawId aDeviceId,
+ const dom::GPUTextureViewDescriptor& aDesc);
+ RawId DeviceCreateSampler(RawId aSelfId,
+ const dom::GPUSamplerDescriptor& aDesc);
+ RawId DeviceCreateCommandEncoder(
+ RawId aSelfId, const dom::GPUCommandEncoderDescriptor& aDesc);
+ RawId CommandEncoderFinish(RawId aSelfId, RawId aDeviceId,
+ const dom::GPUCommandBufferDescriptor& aDesc);
+ RawId RenderBundleEncoderFinish(ffi::WGPURenderBundleEncoder& aEncoder,
+ RawId aDeviceId,
+ const dom::GPURenderBundleDescriptor& aDesc);
+ RawId DeviceCreateBindGroupLayout(
+ RawId aSelfId, const dom::GPUBindGroupLayoutDescriptor& aDesc);
+ RawId DeviceCreatePipelineLayout(
+ RawId aSelfId, const dom::GPUPipelineLayoutDescriptor& aDesc);
+ RawId DeviceCreateBindGroup(RawId aSelfId,
+ const dom::GPUBindGroupDescriptor& aDesc);
+ RawId DeviceCreateComputePipeline(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc);
+ RefPtr<PipelinePromise> DeviceCreateComputePipelineAsync(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc);
+ RawId DeviceCreateRenderPipeline(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc);
+ RefPtr<PipelinePromise> DeviceCreateRenderPipelineAsync(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc);
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<ShaderModule> DeviceCreateShaderModule(
+ Device& aDevice, const dom::GPUShaderModuleDescriptor& aDesc,
+ RefPtr<dom::Promise> aPromise);
+
+ void DeviceCreateSwapChain(RawId aSelfId, const RGBDescriptor& aRgbDesc,
+ size_t maxBufferCount,
+ const layers::RemoteTextureOwnerId& aOwnerId);
+ void SwapChainPresent(RawId aTextureId,
+ const RemoteTextureId& aRemoteTextureId,
+ const RemoteTextureOwnerId& aOwnerId);
+
+ void RegisterDevice(Device* const aDevice);
+ void UnregisterDevice(RawId aId);
+ void FreeUnregisteredInParentDevice(RawId aId);
+
+ static void ConvertTextureFormatRef(const dom::GPUTextureFormat& aInput,
+ ffi::WGPUTextureFormat& aOutput);
+
+ private:
+ virtual ~WebGPUChild();
+
+ void JsWarning(nsIGlobalObject* aGlobal, const nsACString& aMessage);
+
+ RawId DeviceCreateComputePipelineImpl(
+ PipelineCreationContext* const aContext,
+ const dom::GPUComputePipelineDescriptor& aDesc,
+ ipc::ByteBuf* const aByteBuf);
+ RawId DeviceCreateRenderPipelineImpl(
+ PipelineCreationContext* const aContext,
+ const dom::GPURenderPipelineDescriptor& aDesc,
+ ipc::ByteBuf* const aByteBuf);
+
+ UniquePtr<ffi::WGPUClient> const mClient;
+ std::unordered_map<RawId, WeakPtr<Device>> mDeviceMap;
+
+ public:
+ ipc::IPCResult RecvDeviceUncapturedError(RawId aDeviceId,
+ const nsACString& aMessage);
+ ipc::IPCResult RecvDropAction(const ipc::ByteBuf& aByteBuf);
+ void ActorDestroy(ActorDestroyReason) override;
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // WEBGPU_CHILD_H_
diff --git a/dom/webgpu/ipc/WebGPUParent.cpp b/dom/webgpu/ipc/WebGPUParent.cpp
new file mode 100644
index 0000000000..956d531e71
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUParent.cpp
@@ -0,0 +1,1128 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebGPUParent.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/RemoteTextureMap.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/layers/WebRenderImageHost.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+
+namespace mozilla::webgpu {
+
+const uint64_t POLL_TIME_MS = 100;
+
+static mozilla::LazyLogModule sLogger("WebGPU");
+
+// A fixed-capacity buffer for receiving textual error messages from
+// `wgpu_bindings`.
+//
+// The `ToFFI` method returns an `ffi::WGPUErrorBuffer` pointing to our
+// buffer, for you to pass to fallible FFI-visible `wgpu_bindings`
+// functions. These indicate failure by storing an error message in the
+// buffer, which you can retrieve by calling `GetError`.
+//
+// If you call `ToFFI` on this type, you must also call `GetError` to check for
+// an error. Otherwise, the destructor asserts.
+//
+// TODO: refactor this to avoid stack-allocating the buffer all the time.
+class ErrorBuffer {
+ // if the message doesn't fit, it will be truncated
+ static constexpr unsigned BUFFER_SIZE = 512;
+ char mUtf8[BUFFER_SIZE] = {};
+ bool mGuard = false;
+
+ public:
+ ErrorBuffer() { mUtf8[0] = 0; }
+ ErrorBuffer(const ErrorBuffer&) = delete;
+ ~ErrorBuffer() { MOZ_ASSERT(!mGuard); }
+
+ ffi::WGPUErrorBuffer ToFFI() {
+ mGuard = true;
+ ffi::WGPUErrorBuffer errorBuf = {mUtf8, BUFFER_SIZE};
+ return errorBuf;
+ }
+
+ // If an error message was stored in this buffer, return Some(m)
+ // where m is the message as a UTF-8 nsCString. Otherwise, return Nothing.
+ //
+ // Mark this ErrorBuffer as having been handled, so its destructor
+ // won't assert.
+ Maybe<nsCString> GetError() {
+ mGuard = false;
+ if (!mUtf8[0]) {
+ return Nothing();
+ }
+ return Some(nsCString(mUtf8));
+ }
+};
+
+class PresentationData {
+ NS_INLINE_DECL_REFCOUNTING(PresentationData);
+
+ public:
+ RawId mDeviceId = 0;
+ RawId mQueueId = 0;
+ layers::RGBDescriptor mDesc;
+ uint32_t mSourcePitch = 0;
+ int32_t mNextFrameID = 1;
+ std::vector<RawId> mUnassignedBufferIds MOZ_GUARDED_BY(mBuffersLock);
+ std::vector<RawId> mAvailableBufferIds MOZ_GUARDED_BY(mBuffersLock);
+ std::vector<RawId> mQueuedBufferIds MOZ_GUARDED_BY(mBuffersLock);
+ Mutex mBuffersLock;
+
+ PresentationData(RawId aDeviceId, RawId aQueueId,
+ const layers::RGBDescriptor& aDesc, uint32_t aSourcePitch,
+ const nsTArray<RawId>& aBufferIds)
+ : mDeviceId(aDeviceId),
+ mQueueId(aQueueId),
+ mDesc(aDesc),
+ mSourcePitch(aSourcePitch),
+ mBuffersLock("WebGPU presentation buffers") {
+ MOZ_COUNT_CTOR(PresentationData);
+
+ for (const RawId id : aBufferIds) {
+ mUnassignedBufferIds.push_back(id);
+ }
+ }
+
+ private:
+ ~PresentationData() { MOZ_COUNT_DTOR(PresentationData); }
+};
+
+static void FreeAdapter(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_adapter_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeAdapter");
+ }
+}
+static void FreeDevice(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_device_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeDevice");
+ }
+}
+static void FreeShaderModule(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_shader_module_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeShaderModule");
+ }
+}
+static void FreePipelineLayout(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_pipeline_layout_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreePipelineLayout");
+ }
+}
+static void FreeBindGroupLayout(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_bind_group_layout_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeBindGroupLayout");
+ }
+}
+static void FreeBindGroup(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_bind_group_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeBindGroup");
+ }
+}
+static void FreeCommandBuffer(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_command_buffer_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeCommandBuffer");
+ }
+}
+static void FreeRenderBundle(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_render_bundle_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeRenderBundle");
+ }
+}
+static void FreeRenderPipeline(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_render_pipeline_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeRenderPipeline");
+ }
+}
+static void FreeComputePipeline(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_compute_pipeline_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeComputePipeline");
+ }
+}
+static void FreeBuffer(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_buffer_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeBuffer");
+ }
+}
+static void FreeTexture(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_texture_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeTexture");
+ }
+}
+static void FreeTextureView(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_texture_view_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeTextureView");
+ }
+}
+static void FreeSampler(RawId id, void* param) {
+ ipc::ByteBuf byteBuf;
+ wgpu_server_sampler_free(id, ToFFI(&byteBuf));
+ if (!static_cast<WebGPUParent*>(param)->SendDropAction(std::move(byteBuf))) {
+ NS_ERROR("Unable FreeSampler");
+ }
+}
+static void FreeSurface(RawId id, void* param) {
+ Unused << id;
+ Unused << param;
+}
+
+static ffi::WGPUIdentityRecyclerFactory MakeFactory(void* param) {
+ ffi::WGPUIdentityRecyclerFactory factory;
+ PodZero(&factory);
+ factory.param = param;
+ factory.free_adapter = FreeAdapter;
+ factory.free_device = FreeDevice;
+ factory.free_pipeline_layout = FreePipelineLayout;
+ factory.free_shader_module = FreeShaderModule;
+ factory.free_bind_group_layout = FreeBindGroupLayout;
+ factory.free_bind_group = FreeBindGroup;
+ factory.free_command_buffer = FreeCommandBuffer;
+ factory.free_render_bundle = FreeRenderBundle;
+ factory.free_render_pipeline = FreeRenderPipeline;
+ factory.free_compute_pipeline = FreeComputePipeline;
+ factory.free_buffer = FreeBuffer;
+ factory.free_texture = FreeTexture;
+ factory.free_texture_view = FreeTextureView;
+ factory.free_sampler = FreeSampler;
+ factory.free_surface = FreeSurface;
+ return factory;
+}
+
+WebGPUParent::WebGPUParent()
+ : mContext(ffi::wgpu_server_new(MakeFactory(this))) {
+ mTimer.Start(base::TimeDelta::FromMilliseconds(POLL_TIME_MS), this,
+ &WebGPUParent::MaintainDevices);
+}
+
+WebGPUParent::~WebGPUParent() = default;
+
+void WebGPUParent::MaintainDevices() {
+ ffi::wgpu_server_poll_all_devices(mContext.get(), false);
+}
+
+bool WebGPUParent::ForwardError(RawId aDeviceId, ErrorBuffer& aError) {
+ // don't do anything if the error is empty
+ auto cString = aError.GetError();
+ if (!cString) {
+ return false;
+ }
+
+ ReportError(aDeviceId, cString.value());
+
+ return true;
+}
+
+// Generate an error on the Device timeline of aDeviceId.
+// aMessage is interpreted as UTF-8.
+void WebGPUParent::ReportError(RawId aDeviceId, const nsCString& aMessage) {
+ // find the appropriate error scope
+ const auto& lookup = mErrorScopeMap.find(aDeviceId);
+ if (lookup != mErrorScopeMap.end() && !lookup->second.mStack.IsEmpty()) {
+ auto& last = lookup->second.mStack.LastElement();
+ if (last.isNothing()) {
+ last.emplace(ScopedError{false, aMessage});
+ }
+ } else {
+ // fall back to the uncaptured error handler
+ if (!SendDeviceUncapturedError(aDeviceId, aMessage)) {
+ NS_ERROR("Unable to SendError");
+ }
+ }
+}
+
+ipc::IPCResult WebGPUParent::RecvInstanceRequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions,
+ const nsTArray<RawId>& aTargetIds,
+ InstanceRequestAdapterResolver&& resolver) {
+ ffi::WGPURequestAdapterOptions options = {};
+ if (aOptions.mPowerPreference.WasPassed()) {
+ options.power_preference = static_cast<ffi::WGPUPowerPreference>(
+ aOptions.mPowerPreference.Value());
+ }
+ options.force_fallback_adapter = aOptions.mForceFallbackAdapter;
+
+ ErrorBuffer error;
+ int8_t index = ffi::wgpu_server_instance_request_adapter(
+ mContext.get(), &options, aTargetIds.Elements(), aTargetIds.Length(),
+ error.ToFFI());
+
+ ByteBuf infoByteBuf;
+ // Rust side expects an `Option`, so 0 maps to `None`.
+ uint64_t adapterId = 0;
+ if (index >= 0) {
+ adapterId = aTargetIds[index];
+ }
+ ffi::wgpu_server_adapter_pack_info(mContext.get(), adapterId,
+ ToFFI(&infoByteBuf));
+ resolver(std::move(infoByteBuf));
+ ForwardError(0, error);
+
+ // free the unused IDs
+ ipc::ByteBuf dropByteBuf;
+ for (size_t i = 0; i < aTargetIds.Length(); ++i) {
+ if (static_cast<int8_t>(i) != index) {
+ wgpu_server_adapter_free(aTargetIds[i], ToFFI(&dropByteBuf));
+ }
+ }
+ if (dropByteBuf.mData && !SendDropAction(std::move(dropByteBuf))) {
+ NS_ERROR("Unable to free free unused adapter IDs");
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvAdapterRequestDevice(
+ RawId aAdapterId, const ipc::ByteBuf& aByteBuf, RawId aDeviceId,
+ AdapterRequestDeviceResolver&& resolver) {
+ ErrorBuffer error;
+ ffi::wgpu_server_adapter_request_device(
+ mContext.get(), aAdapterId, ToFFI(&aByteBuf), aDeviceId, error.ToFFI());
+ if (ForwardError(0, error)) {
+ resolver(false);
+ } else {
+ mErrorScopeMap.insert({aAdapterId, ErrorScopeStack()});
+ resolver(true);
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvAdapterDestroy(RawId aAdapterId) {
+ ffi::wgpu_server_adapter_drop(mContext.get(), aAdapterId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvDeviceDestroy(RawId aDeviceId) {
+ ffi::wgpu_server_device_drop(mContext.get(), aDeviceId);
+ mErrorScopeMap.erase(aDeviceId);
+ return IPC_OK();
+}
+
+WebGPUParent::BufferMapData* WebGPUParent::GetBufferMapData(RawId aBufferId) {
+ const auto iter = mSharedMemoryMap.find(aBufferId);
+ if (iter == mSharedMemoryMap.end()) {
+ return nullptr;
+ }
+
+ return &iter->second;
+}
+
+ipc::IPCResult WebGPUParent::RecvCreateBuffer(
+ RawId aDeviceId, RawId aBufferId, dom::GPUBufferDescriptor&& aDesc,
+ ipc::UnsafeSharedMemoryHandle&& aShmem) {
+ webgpu::StringHelper label(aDesc.mLabel);
+
+ auto shmem =
+ ipc::WritableSharedMemoryMapping::Open(std::move(aShmem)).value();
+
+ bool hasMapFlags = aDesc.mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
+ dom::GPUBufferUsage_Binding::MAP_READ);
+ if (hasMapFlags || aDesc.mMappedAtCreation) {
+ uint64_t offset = 0;
+ uint64_t size = 0;
+ if (aDesc.mMappedAtCreation) {
+ size = aDesc.mSize;
+ MOZ_RELEASE_ASSERT(shmem.Size() >= aDesc.mSize);
+ }
+
+ BufferMapData data = {std::move(shmem), hasMapFlags, offset, size};
+ mSharedMemoryMap.insert({aBufferId, std::move(data)});
+ }
+
+ ErrorBuffer error;
+ ffi::wgpu_server_device_create_buffer(mContext.get(), aDeviceId, aBufferId,
+ label.Get(), aDesc.mSize, aDesc.mUsage,
+ aDesc.mMappedAtCreation, error.ToFFI());
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+struct MapRequest {
+ RefPtr<WebGPUParent> mParent;
+ ffi::WGPUGlobal* mContext;
+ ffi::WGPUBufferId mBufferId;
+ ffi::WGPUHostMap mHostMap;
+ uint64_t mOffset;
+ uint64_t mSize;
+ WebGPUParent::BufferMapResolver mResolver;
+};
+
+static const char* MapStatusString(ffi::WGPUBufferMapAsyncStatus status) {
+ switch (status) {
+ case ffi::WGPUBufferMapAsyncStatus_Success:
+ return "Success";
+ case ffi::WGPUBufferMapAsyncStatus_AlreadyMapped:
+ return "Already mapped";
+ case ffi::WGPUBufferMapAsyncStatus_MapAlreadyPending:
+ return "Map is already pending";
+ case ffi::WGPUBufferMapAsyncStatus_Aborted:
+ return "Map aborted";
+ case ffi::WGPUBufferMapAsyncStatus_ContextLost:
+ return "Context lost";
+ case ffi::WGPUBufferMapAsyncStatus_Invalid:
+ return "Invalid buffer";
+ case ffi::WGPUBufferMapAsyncStatus_InvalidRange:
+ return "Invalid range";
+ case ffi::WGPUBufferMapAsyncStatus_InvalidAlignment:
+ return "Invalid alignment";
+ case ffi::WGPUBufferMapAsyncStatus_InvalidUsageFlags:
+ return "Invalid usage flags";
+ case ffi::WGPUBufferMapAsyncStatus_Error:
+ return "Map failed";
+ case ffi::WGPUBufferMapAsyncStatus_Sentinel: // For -Wswitch
+ break;
+ }
+
+ MOZ_CRASH("Bad ffi::WGPUBufferMapAsyncStatus");
+}
+
+static void MapCallback(ffi::WGPUBufferMapAsyncStatus status,
+ uint8_t* userdata) {
+ auto* req = reinterpret_cast<MapRequest*>(userdata);
+
+ if (!req->mParent->CanSend()) {
+ delete req;
+ return;
+ }
+
+ BufferMapResult result;
+
+ auto bufferId = req->mBufferId;
+ auto* mapData = req->mParent->GetBufferMapData(bufferId);
+ MOZ_RELEASE_ASSERT(mapData);
+
+ if (status != ffi::WGPUBufferMapAsyncStatus_Success) {
+ result = BufferMapError(nsPrintfCString("Mapping WebGPU buffer failed: %s",
+ MapStatusString(status)));
+ } else {
+ auto size = req->mSize;
+ auto offset = req->mOffset;
+
+ if (req->mHostMap == ffi::WGPUHostMap_Read && size > 0) {
+ const auto src = ffi::wgpu_server_buffer_get_mapped_range(
+ req->mContext, req->mBufferId, offset, size);
+
+ MOZ_RELEASE_ASSERT(mapData->mShmem.Size() >= offset + size);
+ if (src.ptr != nullptr && src.length >= size) {
+ auto dst = mapData->mShmem.Bytes().Subspan(offset, size);
+ memcpy(dst.data(), src.ptr, size);
+ }
+ }
+
+ result =
+ BufferMapSuccess(offset, size, req->mHostMap == ffi::WGPUHostMap_Write);
+
+ mapData->mMappedOffset = offset;
+ mapData->mMappedSize = size;
+ }
+
+ req->mResolver(std::move(result));
+ delete req;
+}
+
+ipc::IPCResult WebGPUParent::RecvBufferMap(RawId aBufferId, uint32_t aMode,
+ uint64_t aOffset, uint64_t aSize,
+ BufferMapResolver&& aResolver) {
+ MOZ_LOG(sLogger, LogLevel::Info,
+ ("RecvBufferMap %" PRIu64 " offset=%" PRIu64 " size=%" PRIu64 "\n",
+ aBufferId, aOffset, aSize));
+
+ ffi::WGPUHostMap mode;
+ switch (aMode) {
+ case dom::GPUMapMode_Binding::READ:
+ mode = ffi::WGPUHostMap_Read;
+ break;
+ case dom::GPUMapMode_Binding::WRITE:
+ mode = ffi::WGPUHostMap_Write;
+ break;
+ default: {
+ nsCString errorString(
+ "GPUBuffer.mapAsync 'mode' argument must be either GPUMapMode.READ "
+ "or GPUMapMode.WRITE");
+ aResolver(BufferMapError(errorString));
+ return IPC_OK();
+ }
+ }
+
+ auto* mapData = GetBufferMapData(aBufferId);
+
+ if (!mapData) {
+ nsCString errorString("Buffer is not mappable");
+ aResolver(BufferMapError(errorString));
+ return IPC_OK();
+ }
+
+ auto* request =
+ new MapRequest{this, mContext.get(), aBufferId, mode,
+ aOffset, aSize, std::move(aResolver)};
+
+ ffi::WGPUBufferMapCallbackC callback = {&MapCallback,
+ reinterpret_cast<uint8_t*>(request)};
+ ffi::wgpu_server_buffer_map(mContext.get(), aBufferId, aOffset, aSize, mode,
+ callback);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvBufferUnmap(RawId aDeviceId, RawId aBufferId,
+ bool aFlush) {
+ MOZ_LOG(sLogger, LogLevel::Info,
+ ("RecvBufferUnmap %" PRIu64 " flush=%d\n", aBufferId, aFlush));
+
+ auto* mapData = GetBufferMapData(aBufferId);
+
+ if (mapData && aFlush) {
+ uint64_t offset = mapData->mMappedOffset;
+ uint64_t size = mapData->mMappedSize;
+
+ const auto mapped = ffi::wgpu_server_buffer_get_mapped_range(
+ mContext.get(), aBufferId, offset, size);
+
+ if (mapped.ptr != nullptr && mapped.length >= size) {
+ auto shmSize = mapData->mShmem.Size();
+ MOZ_RELEASE_ASSERT(offset <= shmSize);
+ MOZ_RELEASE_ASSERT(size <= shmSize - offset);
+
+ auto src = mapData->mShmem.Bytes().Subspan(offset, size);
+ memcpy(mapped.ptr, src.data(), size);
+ }
+
+ mapData->mMappedOffset = 0;
+ mapData->mMappedSize = 0;
+ }
+
+ ErrorBuffer error;
+ ffi::wgpu_server_buffer_unmap(mContext.get(), aBufferId, error.ToFFI());
+ ForwardError(aDeviceId, error);
+
+ if (mapData && !mapData->mHasMapFlags) {
+ // We get here if the buffer was mapped at creation without map flags.
+ // We don't need the shared memory anymore.
+ DeallocBufferShmem(aBufferId);
+ }
+
+ return IPC_OK();
+}
+
+void WebGPUParent::DeallocBufferShmem(RawId aBufferId) {
+ const auto iter = mSharedMemoryMap.find(aBufferId);
+ if (iter != mSharedMemoryMap.end()) {
+ mSharedMemoryMap.erase(iter);
+ }
+}
+
+ipc::IPCResult WebGPUParent::RecvBufferDrop(RawId aBufferId) {
+ ffi::wgpu_server_buffer_drop(mContext.get(), aBufferId);
+ MOZ_LOG(sLogger, LogLevel::Info, ("RecvBufferDrop %" PRIu64 "\n", aBufferId));
+
+ DeallocBufferShmem(aBufferId);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvBufferDestroy(RawId aBufferId) {
+ ffi::wgpu_server_buffer_destroy(mContext.get(), aBufferId);
+ MOZ_LOG(sLogger, LogLevel::Info,
+ ("RecvBufferDestroy %" PRIu64 "\n", aBufferId));
+
+ DeallocBufferShmem(aBufferId);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvTextureDestroy(RawId aTextureId) {
+ ffi::wgpu_server_texture_drop(mContext.get(), aTextureId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvTextureViewDestroy(RawId aTextureViewId) {
+ ffi::wgpu_server_texture_view_drop(mContext.get(), aTextureViewId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvSamplerDestroy(RawId aSamplerId) {
+ ffi::wgpu_server_sampler_drop(mContext.get(), aSamplerId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvCommandEncoderFinish(
+ RawId aEncoderId, RawId aDeviceId,
+ const dom::GPUCommandBufferDescriptor& aDesc) {
+ Unused << aDesc;
+ ffi::WGPUCommandBufferDescriptor desc = {};
+
+ webgpu::StringHelper label(aDesc.mLabel);
+ desc.label = label.Get();
+
+ ErrorBuffer error;
+ ffi::wgpu_server_encoder_finish(mContext.get(), aEncoderId, &desc,
+ error.ToFFI());
+
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvCommandEncoderDestroy(RawId aEncoderId) {
+ ffi::wgpu_server_encoder_drop(mContext.get(), aEncoderId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvCommandBufferDestroy(RawId aCommandBufferId) {
+ ffi::wgpu_server_command_buffer_drop(mContext.get(), aCommandBufferId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvRenderBundleDestroy(RawId aBundleId) {
+ ffi::wgpu_server_render_bundle_drop(mContext.get(), aBundleId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvQueueSubmit(
+ RawId aQueueId, RawId aDeviceId, const nsTArray<RawId>& aCommandBuffers) {
+ ErrorBuffer error;
+ ffi::wgpu_server_queue_submit(mContext.get(), aQueueId,
+ aCommandBuffers.Elements(),
+ aCommandBuffers.Length(), error.ToFFI());
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvQueueWriteAction(
+ RawId aQueueId, RawId aDeviceId, const ipc::ByteBuf& aByteBuf,
+ ipc::UnsafeSharedMemoryHandle&& aShmem) {
+ auto mapping =
+ ipc::WritableSharedMemoryMapping::Open(std::move(aShmem)).value();
+
+ ErrorBuffer error;
+ ffi::wgpu_server_queue_write_action(mContext.get(), aQueueId,
+ ToFFI(&aByteBuf), mapping.Bytes().data(),
+ mapping.Size(), error.ToFFI());
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvBindGroupLayoutDestroy(RawId aBindGroupId) {
+ ffi::wgpu_server_bind_group_layout_drop(mContext.get(), aBindGroupId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvPipelineLayoutDestroy(RawId aLayoutId) {
+ ffi::wgpu_server_pipeline_layout_drop(mContext.get(), aLayoutId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvBindGroupDestroy(RawId aBindGroupId) {
+ ffi::wgpu_server_bind_group_drop(mContext.get(), aBindGroupId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvShaderModuleDestroy(RawId aModuleId) {
+ ffi::wgpu_server_shader_module_drop(mContext.get(), aModuleId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvComputePipelineDestroy(RawId aPipelineId) {
+ ffi::wgpu_server_compute_pipeline_drop(mContext.get(), aPipelineId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvRenderPipelineDestroy(RawId aPipelineId) {
+ ffi::wgpu_server_render_pipeline_drop(mContext.get(), aPipelineId);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvImplicitLayoutDestroy(
+ RawId aImplicitPlId, const nsTArray<RawId>& aImplicitBglIds) {
+ ffi::wgpu_server_pipeline_layout_drop(mContext.get(), aImplicitPlId);
+ for (const auto& id : aImplicitBglIds) {
+ ffi::wgpu_server_bind_group_layout_drop(mContext.get(), id);
+ }
+ return IPC_OK();
+}
+
+// TODO: proper destruction
+
+ipc::IPCResult WebGPUParent::RecvDeviceCreateSwapChain(
+ RawId aDeviceId, RawId aQueueId, const RGBDescriptor& aDesc,
+ const nsTArray<RawId>& aBufferIds,
+ const layers::RemoteTextureOwnerId& aOwnerId) {
+ switch (aDesc.format()) {
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid surface format!");
+ return IPC_OK();
+ }
+
+ constexpr uint32_t kBufferAlignmentMask = 0xff;
+ const auto bufferStrideWithMask = CheckedInt<uint32_t>(aDesc.size().width) *
+ gfx::BytesPerPixel(aDesc.format()) +
+ kBufferAlignmentMask;
+ if (!bufferStrideWithMask.isValid()) {
+ MOZ_ASSERT_UNREACHABLE("Invalid width / buffer stride!");
+ return IPC_OK();
+ }
+
+ const uint32_t bufferStride =
+ bufferStrideWithMask.value() & ~kBufferAlignmentMask;
+
+ const auto rows = CheckedInt<uint32_t>(aDesc.size().height);
+ if (!rows.isValid()) {
+ MOZ_ASSERT_UNREACHABLE("Invalid height!");
+ return IPC_OK();
+ }
+
+ if (!mRemoteTextureOwner) {
+ mRemoteTextureOwner =
+ MakeRefPtr<layers::RemoteTextureOwnerClient>(OtherPid());
+ }
+ // RemoteTextureMap::GetRemoteTextureForDisplayList() works synchronously.
+ mRemoteTextureOwner->RegisterTextureOwner(aOwnerId, /* aIsSyncMode */ true);
+
+ auto data = MakeRefPtr<PresentationData>(aDeviceId, aQueueId, aDesc,
+ bufferStride, aBufferIds);
+ if (!mCanvasMap.emplace(aOwnerId, data).second) {
+ NS_ERROR("External image is already registered as WebGPU canvas!");
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvDeviceCreateShaderModule(
+ RawId aDeviceId, RawId aModuleId, const nsString& aLabel,
+ const nsCString& aCode, DeviceCreateShaderModuleResolver&& aOutMessage) {
+ // TODO: this should probably be an optional label in the IPC message.
+ const nsACString* label = nullptr;
+ NS_ConvertUTF16toUTF8 utf8Label(aLabel);
+ if (!utf8Label.IsEmpty()) {
+ label = &utf8Label;
+ }
+
+ ffi::WGPUShaderModuleCompilationMessage message;
+
+ bool ok = ffi::wgpu_server_device_create_shader_module(
+ mContext.get(), aDeviceId, aModuleId, label, &aCode, &message);
+
+ nsTArray<WebGPUCompilationMessage> messages;
+
+ if (!ok) {
+ WebGPUCompilationMessage msg;
+ msg.lineNum = message.line_number;
+ msg.linePos = message.line_pos;
+ msg.offset = message.utf16_offset;
+ msg.length = message.utf16_length;
+ msg.message = message.message;
+ // wgpu currently only returns errors.
+ msg.messageType = WebGPUCompilationMessageType::Error;
+
+ messages.AppendElement(msg);
+ }
+
+ aOutMessage(messages);
+
+ return IPC_OK();
+}
+
+struct PresentRequest {
+ PresentRequest(const ffi::WGPUGlobal* aContext,
+ RefPtr<PresentationData>& aData,
+ RefPtr<layers::RemoteTextureOwnerClient>& aRemoteTextureOwner,
+ const layers::RemoteTextureId aTextureId,
+ const layers::RemoteTextureOwnerId aOwnerId)
+ : mContext(aContext),
+ mData(aData),
+ mRemoteTextureOwner(aRemoteTextureOwner),
+ mTextureId(aTextureId),
+ mOwnerId(aOwnerId) {}
+
+ const ffi::WGPUGlobal* mContext;
+ RefPtr<PresentationData> mData;
+ RefPtr<layers::RemoteTextureOwnerClient> mRemoteTextureOwner;
+ const layers::RemoteTextureId mTextureId;
+ const layers::RemoteTextureOwnerId mOwnerId;
+};
+
+static void PresentCallback(ffi::WGPUBufferMapAsyncStatus status,
+ uint8_t* userdata) {
+ UniquePtr<PresentRequest> req(reinterpret_cast<PresentRequest*>(userdata));
+
+ if (!req->mRemoteTextureOwner->IsRegistered(req->mOwnerId)) {
+ // SwapChain is already Destroyed
+ return;
+ }
+
+ PresentationData* data = req->mData.get();
+ // get the buffer ID
+ RawId bufferId;
+ {
+ MutexAutoLock lock(data->mBuffersLock);
+ bufferId = data->mQueuedBufferIds.back();
+ data->mQueuedBufferIds.pop_back();
+ }
+
+ // Ensure we'll make the bufferId available for reuse
+ auto releaseBuffer = MakeScopeExit([data = RefPtr{data}, bufferId] {
+ MutexAutoLock lock(data->mBuffersLock);
+ data->mAvailableBufferIds.push_back(bufferId);
+ });
+
+ MOZ_LOG(
+ sLogger, LogLevel::Info,
+ ("PresentCallback for buffer %" PRIu64 " status=%d\n", bufferId, status));
+ // copy the data
+ if (status == ffi::WGPUBufferMapAsyncStatus_Success) {
+ const auto bufferSize = data->mDesc.size().height * data->mSourcePitch;
+ const auto mapped = ffi::wgpu_server_buffer_get_mapped_range(
+ req->mContext, bufferId, 0, bufferSize);
+ MOZ_ASSERT(mapped.length >= bufferSize);
+ auto textureData =
+ req->mRemoteTextureOwner->CreateOrRecycleBufferTextureData(
+ req->mOwnerId, data->mDesc.size(), data->mDesc.format());
+ if (!textureData) {
+ gfxCriticalNoteOnce << "Failed to allocate BufferTextureData";
+ return;
+ }
+ layers::MappedTextureData mappedData;
+ if (textureData && textureData->BorrowMappedData(mappedData)) {
+ uint8_t* src = mapped.ptr;
+ uint8_t* dst = mappedData.data;
+ for (auto row = 0; row < data->mDesc.size().height; ++row) {
+ memcpy(dst, src, mappedData.stride);
+ dst += mappedData.stride;
+ src += data->mSourcePitch;
+ }
+ req->mRemoteTextureOwner->PushTexture(req->mTextureId, req->mOwnerId,
+ std::move(textureData),
+ /* aSharedSurface */ nullptr);
+ } else {
+ NS_WARNING("WebGPU present skipped: the swapchain is resized!");
+ }
+ ErrorBuffer error;
+ wgpu_server_buffer_unmap(req->mContext, bufferId, error.ToFFI());
+ if (auto errorString = error.GetError()) {
+ MOZ_LOG(
+ sLogger, LogLevel::Info,
+ ("WebGPU present: buffer unmap failed: %s\n", errorString->get()));
+ }
+ } else {
+ // TODO: better handle errors
+ NS_WARNING("WebGPU frame mapping failed!");
+ }
+}
+
+ipc::IPCResult WebGPUParent::GetFrontBufferSnapshot(
+ IProtocol* aProtocol, const layers::RemoteTextureOwnerId& aOwnerId,
+ Maybe<Shmem>& aShmem, gfx::IntSize& aSize) {
+ const auto& lookup = mCanvasMap.find(aOwnerId);
+ if (lookup == mCanvasMap.end() || !mRemoteTextureOwner) {
+ return IPC_OK();
+ }
+
+ RefPtr<PresentationData> data = lookup->second.get();
+ aSize = data->mDesc.size();
+ uint32_t stride = layers::ImageDataSerializer::ComputeRGBStride(
+ data->mDesc.format(), aSize.width);
+ uint32_t len = data->mDesc.size().height * stride;
+ Shmem shmem;
+ if (!AllocShmem(len, &shmem)) {
+ return IPC_OK();
+ }
+
+ mRemoteTextureOwner->GetLatestBufferSnapshot(aOwnerId, shmem, aSize);
+ aShmem.emplace(std::move(shmem));
+
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvSwapChainPresent(
+ RawId aTextureId, RawId aCommandEncoderId,
+ const layers::RemoteTextureId& aRemoteTextureId,
+ const layers::RemoteTextureOwnerId& aOwnerId) {
+ // step 0: get the data associated with the swapchain
+ const auto& lookup = mCanvasMap.find(aOwnerId);
+ if (lookup == mCanvasMap.end() || !mRemoteTextureOwner ||
+ !mRemoteTextureOwner->IsRegistered(aOwnerId)) {
+ NS_WARNING("WebGPU presenting on a destroyed swap chain!");
+ return IPC_OK();
+ }
+
+ RefPtr<PresentationData> data = lookup->second.get();
+ RawId bufferId = 0;
+ const auto& size = data->mDesc.size();
+ const auto bufferSize = data->mDesc.size().height * data->mSourcePitch;
+
+ // step 1: find an available staging buffer, or create one
+ {
+ MutexAutoLock lock(data->mBuffersLock);
+ if (!data->mAvailableBufferIds.empty()) {
+ bufferId = data->mAvailableBufferIds.back();
+ data->mAvailableBufferIds.pop_back();
+ } else if (!data->mUnassignedBufferIds.empty()) {
+ bufferId = data->mUnassignedBufferIds.back();
+ data->mUnassignedBufferIds.pop_back();
+
+ ffi::WGPUBufferUsages usage =
+ WGPUBufferUsages_COPY_DST | WGPUBufferUsages_MAP_READ;
+
+ ErrorBuffer error;
+ ffi::wgpu_server_device_create_buffer(mContext.get(), data->mDeviceId,
+ bufferId, nullptr, bufferSize,
+ usage, false, error.ToFFI());
+ if (ForwardError(data->mDeviceId, error)) {
+ return IPC_OK();
+ }
+ } else {
+ bufferId = 0;
+ }
+
+ if (bufferId) {
+ data->mQueuedBufferIds.insert(data->mQueuedBufferIds.begin(), bufferId);
+ }
+ }
+
+ MOZ_LOG(sLogger, LogLevel::Info,
+ ("RecvSwapChainPresent with buffer %" PRIu64 "\n", bufferId));
+ if (!bufferId) {
+ // TODO: add a warning - no buffer are available!
+ return IPC_OK();
+ }
+
+ // step 3: submit a copy command for the frame
+ ffi::WGPUCommandEncoderDescriptor encoderDesc = {};
+ {
+ ErrorBuffer error;
+ ffi::wgpu_server_device_create_encoder(mContext.get(), data->mDeviceId,
+ &encoderDesc, aCommandEncoderId,
+ error.ToFFI());
+ if (ForwardError(data->mDeviceId, error)) {
+ return IPC_OK();
+ }
+ }
+
+ const ffi::WGPUImageCopyTexture texView = {
+ aTextureId,
+ };
+ const ffi::WGPUImageDataLayout bufLayout = {
+ 0,
+ &data->mSourcePitch,
+ nullptr,
+ };
+ const ffi::WGPUExtent3d extent = {
+ static_cast<uint32_t>(size.width),
+ static_cast<uint32_t>(size.height),
+ 1,
+ };
+ ffi::wgpu_server_encoder_copy_texture_to_buffer(
+ mContext.get(), aCommandEncoderId, &texView, bufferId, &bufLayout,
+ &extent);
+ ffi::WGPUCommandBufferDescriptor commandDesc = {};
+ {
+ ErrorBuffer error;
+ ffi::wgpu_server_encoder_finish(mContext.get(), aCommandEncoderId,
+ &commandDesc, error.ToFFI());
+ if (ForwardError(data->mDeviceId, error)) {
+ return IPC_OK();
+ }
+ }
+
+ {
+ ErrorBuffer error;
+ ffi::wgpu_server_queue_submit(mContext.get(), data->mQueueId,
+ &aCommandEncoderId, 1, error.ToFFI());
+ if (ForwardError(data->mDeviceId, error)) {
+ return IPC_OK();
+ }
+ }
+
+ // step 4: request the pixels to be copied into the external texture
+ // TODO: this isn't strictly necessary. When WR wants to Lock() the external
+ // texture,
+ // we can just give it the contents of the last mapped buffer instead of the
+ // copy.
+ auto presentRequest = MakeUnique<PresentRequest>(
+ mContext.get(), data, mRemoteTextureOwner, aRemoteTextureId, aOwnerId);
+
+ ffi::WGPUBufferMapCallbackC callback = {
+ &PresentCallback, reinterpret_cast<uint8_t*>(presentRequest.release())};
+ ffi::wgpu_server_buffer_map(mContext.get(), bufferId, 0, bufferSize,
+ ffi::WGPUHostMap_Read, callback);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvSwapChainDestroy(
+ const layers::RemoteTextureOwnerId& aOwnerId) {
+ if (mRemoteTextureOwner) {
+ mRemoteTextureOwner->UnregisterTextureOwner(aOwnerId);
+ }
+ const auto& lookup = mCanvasMap.find(aOwnerId);
+ MOZ_ASSERT(lookup != mCanvasMap.end());
+ if (lookup == mCanvasMap.end()) {
+ NS_WARNING("WebGPU presenting on a destroyed swap chain!");
+ return IPC_OK();
+ }
+
+ RefPtr<PresentationData> data = lookup->second.get();
+ mCanvasMap.erase(lookup);
+
+ MutexAutoLock lock(data->mBuffersLock);
+ ipc::ByteBuf dropByteBuf;
+ for (const auto bid : data->mUnassignedBufferIds) {
+ wgpu_server_buffer_free(bid, ToFFI(&dropByteBuf));
+ }
+ if (dropByteBuf.mData && !SendDropAction(std::move(dropByteBuf))) {
+ NS_WARNING("Unable to free an ID for non-assigned buffer");
+ }
+ for (const auto bid : data->mAvailableBufferIds) {
+ ffi::wgpu_server_buffer_drop(mContext.get(), bid);
+ }
+ for (const auto bid : data->mQueuedBufferIds) {
+ ffi::wgpu_server_buffer_drop(mContext.get(), bid);
+ }
+ return IPC_OK();
+}
+
+void WebGPUParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mTimer.Stop();
+ mCanvasMap.clear();
+ if (mRemoteTextureOwner) {
+ mRemoteTextureOwner->UnregisterAllTextureOwners();
+ mRemoteTextureOwner = nullptr;
+ }
+ ffi::wgpu_server_poll_all_devices(mContext.get(), true);
+ mContext = nullptr;
+}
+
+ipc::IPCResult WebGPUParent::RecvDeviceAction(RawId aDeviceId,
+ const ipc::ByteBuf& aByteBuf) {
+ ErrorBuffer error;
+ ffi::wgpu_server_device_action(mContext.get(), aDeviceId, ToFFI(&aByteBuf),
+ error.ToFFI());
+
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvDeviceActionWithAck(
+ RawId aDeviceId, const ipc::ByteBuf& aByteBuf,
+ DeviceActionWithAckResolver&& aResolver) {
+ ErrorBuffer error;
+ ffi::wgpu_server_device_action(mContext.get(), aDeviceId, ToFFI(&aByteBuf),
+ error.ToFFI());
+
+ ForwardError(aDeviceId, error);
+ aResolver(true);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvTextureAction(RawId aTextureId,
+ RawId aDeviceId,
+ const ipc::ByteBuf& aByteBuf) {
+ ErrorBuffer error;
+ ffi::wgpu_server_texture_action(mContext.get(), aTextureId, ToFFI(&aByteBuf),
+ error.ToFFI());
+
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvCommandEncoderAction(
+ RawId aEncoderId, RawId aDeviceId, const ipc::ByteBuf& aByteBuf) {
+ ErrorBuffer error;
+ ffi::wgpu_server_command_encoder_action(mContext.get(), aEncoderId,
+ ToFFI(&aByteBuf), error.ToFFI());
+ ForwardError(aDeviceId, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvBumpImplicitBindGroupLayout(RawId aPipelineId,
+ bool aIsCompute,
+ uint32_t aIndex,
+ RawId aAssignId) {
+ ErrorBuffer error;
+ if (aIsCompute) {
+ ffi::wgpu_server_compute_pipeline_get_bind_group_layout(
+ mContext.get(), aPipelineId, aIndex, aAssignId, error.ToFFI());
+ } else {
+ ffi::wgpu_server_render_pipeline_get_bind_group_layout(
+ mContext.get(), aPipelineId, aIndex, aAssignId, error.ToFFI());
+ }
+
+ ForwardError(0, error);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvDevicePushErrorScope(RawId aDeviceId) {
+ const auto& lookup = mErrorScopeMap.find(aDeviceId);
+ if (lookup == mErrorScopeMap.end()) {
+ // Content can cause this simply by destroying a device and then
+ // calling `pushErrorScope`.
+ return IPC_OK();
+ }
+
+ lookup->second.mStack.EmplaceBack();
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvDevicePopErrorScope(
+ RawId aDeviceId, DevicePopErrorScopeResolver&& aResolver) {
+ const auto& lookup = mErrorScopeMap.find(aDeviceId);
+ if (lookup == mErrorScopeMap.end()) {
+ // Content can cause this simply by destroying a device and then
+ // calling `popErrorScope`.
+ ScopedError error = {true};
+ aResolver(Some(error));
+ return IPC_OK();
+ }
+
+ if (lookup->second.mStack.IsEmpty()) {
+ // Content can cause this simply by calling `popErrorScope` when
+ // there is no error scope pushed.
+ ScopedError error = {true};
+ aResolver(Some(error));
+ return IPC_OK();
+ }
+
+ auto scope = lookup->second.mStack.PopLastElement();
+ aResolver(scope);
+ return IPC_OK();
+}
+
+ipc::IPCResult WebGPUParent::RecvGenerateError(RawId aDeviceId,
+ const nsCString& aMessage) {
+ ReportError(aDeviceId, aMessage);
+ return IPC_OK();
+}
+
+} // namespace mozilla::webgpu
diff --git a/dom/webgpu/ipc/WebGPUParent.h b/dom/webgpu/ipc/WebGPUParent.h
new file mode 100644
index 0000000000..384d560003
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUParent.h
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBGPU_PARENT_H_
+#define WEBGPU_PARENT_H_
+
+#include "mozilla/webgpu/ffi/wgpu.h"
+#include "mozilla/webgpu/PWebGPUParent.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/ipc/RawShmem.h"
+#include "WebGPUTypes.h"
+#include "base/timer.h"
+
+namespace mozilla {
+
+namespace layers {
+class RemoteTextureOwnerClient;
+} // namespace layers
+
+namespace webgpu {
+
+class ErrorBuffer;
+class PresentationData;
+
+struct ErrorScopeStack {
+ nsTArray<MaybeScopedError> mStack;
+};
+
+class WebGPUParent final : public PWebGPUParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebGPUParent, override)
+
+ public:
+ explicit WebGPUParent();
+
+ ipc::IPCResult RecvInstanceRequestAdapter(
+ const dom::GPURequestAdapterOptions& aOptions,
+ const nsTArray<RawId>& aTargetIds,
+ InstanceRequestAdapterResolver&& resolver);
+ ipc::IPCResult RecvAdapterRequestDevice(
+ RawId aAdapterId, const ipc::ByteBuf& aByteBuf, RawId aDeviceId,
+ AdapterRequestDeviceResolver&& resolver);
+ ipc::IPCResult RecvAdapterDestroy(RawId aAdapterId);
+ ipc::IPCResult RecvDeviceDestroy(RawId aDeviceId);
+ ipc::IPCResult RecvCreateBuffer(RawId aDeviceId, RawId aBufferId,
+ dom::GPUBufferDescriptor&& aDesc,
+ ipc::UnsafeSharedMemoryHandle&& aShmem);
+ ipc::IPCResult RecvBufferMap(RawId aBufferId, uint32_t aMode,
+ uint64_t aOffset, uint64_t size,
+ BufferMapResolver&& aResolver);
+ ipc::IPCResult RecvBufferUnmap(RawId aDeviceId, RawId aBufferId, bool aFlush);
+ ipc::IPCResult RecvBufferDestroy(RawId aBufferId);
+ ipc::IPCResult RecvBufferDrop(RawId aBufferId);
+ ipc::IPCResult RecvTextureDestroy(RawId aTextureId);
+ ipc::IPCResult RecvTextureViewDestroy(RawId aTextureViewId);
+ ipc::IPCResult RecvSamplerDestroy(RawId aSamplerId);
+ ipc::IPCResult RecvCommandEncoderFinish(
+ RawId aEncoderId, RawId aDeviceId,
+ const dom::GPUCommandBufferDescriptor& aDesc);
+ ipc::IPCResult RecvCommandEncoderDestroy(RawId aEncoderId);
+ ipc::IPCResult RecvCommandBufferDestroy(RawId aCommandBufferId);
+ ipc::IPCResult RecvRenderBundleDestroy(RawId aBundleId);
+ ipc::IPCResult RecvQueueSubmit(RawId aQueueId, RawId aDeviceId,
+ const nsTArray<RawId>& aCommandBuffers);
+ ipc::IPCResult RecvQueueWriteAction(RawId aQueueId, RawId aDeviceId,
+ const ipc::ByteBuf& aByteBuf,
+ ipc::UnsafeSharedMemoryHandle&& aShmem);
+ ipc::IPCResult RecvBindGroupLayoutDestroy(RawId aBindGroupLayoutId);
+ ipc::IPCResult RecvPipelineLayoutDestroy(RawId aPipelineLayoutId);
+ ipc::IPCResult RecvBindGroupDestroy(RawId aBindGroupId);
+ ipc::IPCResult RecvShaderModuleDestroy(RawId aModuleId);
+ ipc::IPCResult RecvComputePipelineDestroy(RawId aPipelineId);
+ ipc::IPCResult RecvRenderPipelineDestroy(RawId aPipelineId);
+ ipc::IPCResult RecvImplicitLayoutDestroy(
+ RawId aImplicitPlId, const nsTArray<RawId>& aImplicitBglIds);
+ ipc::IPCResult RecvDeviceCreateSwapChain(
+ RawId aDeviceId, RawId aQueueId, const layers::RGBDescriptor& aDesc,
+ const nsTArray<RawId>& aBufferIds,
+ const layers::RemoteTextureOwnerId& aOwnerId);
+ ipc::IPCResult RecvDeviceCreateShaderModule(
+ RawId aDeviceId, RawId aModuleId, const nsString& aLabel,
+ const nsCString& aCode, DeviceCreateShaderModuleResolver&& aOutMessage);
+
+ ipc::IPCResult RecvSwapChainPresent(
+ RawId aTextureId, RawId aCommandEncoderId,
+ const layers::RemoteTextureId& aRemoteTextureId,
+ const layers::RemoteTextureOwnerId& aOwnerId);
+ ipc::IPCResult RecvSwapChainDestroy(
+ const layers::RemoteTextureOwnerId& aOwnerId);
+
+ ipc::IPCResult RecvDeviceAction(RawId aDeviceId,
+ const ipc::ByteBuf& aByteBuf);
+ ipc::IPCResult RecvDeviceActionWithAck(
+ RawId aDeviceId, const ipc::ByteBuf& aByteBuf,
+ DeviceActionWithAckResolver&& aResolver);
+ ipc::IPCResult RecvTextureAction(RawId aTextureId, RawId aDevice,
+ const ipc::ByteBuf& aByteBuf);
+ ipc::IPCResult RecvCommandEncoderAction(RawId aEncoderId, RawId aDeviceId,
+ const ipc::ByteBuf& aByteBuf);
+ ipc::IPCResult RecvBumpImplicitBindGroupLayout(RawId aPipelineId,
+ bool aIsCompute,
+ uint32_t aIndex,
+ RawId aAssignId);
+
+ ipc::IPCResult RecvDevicePushErrorScope(RawId aDeviceId);
+ ipc::IPCResult RecvDevicePopErrorScope(
+ RawId aDeviceId, DevicePopErrorScopeResolver&& aResolver);
+ ipc::IPCResult RecvGenerateError(RawId aDeviceId, const nsCString& message);
+
+ ipc::IPCResult GetFrontBufferSnapshot(
+ IProtocol* aProtocol, const layers::RemoteTextureOwnerId& aOwnerId,
+ Maybe<Shmem>& aShmem, gfx::IntSize& aSize);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ struct BufferMapData {
+ ipc::WritableSharedMemoryMapping mShmem;
+ // True if buffer's usage has MAP_READ or MAP_WRITE set.
+ bool mHasMapFlags;
+ uint64_t mMappedOffset;
+ uint64_t mMappedSize;
+ };
+
+ BufferMapData* GetBufferMapData(RawId aBufferId);
+
+ private:
+ void DeallocBufferShmem(RawId aBufferId);
+
+ virtual ~WebGPUParent();
+ void MaintainDevices();
+ bool ForwardError(RawId aDeviceId, ErrorBuffer& aError);
+ void ReportError(RawId aDeviceId, const nsCString& message);
+
+ UniquePtr<ffi::WGPUGlobal> mContext;
+ base::RepeatingTimer<WebGPUParent> mTimer;
+
+ /// A map from wgpu buffer ids to data about their shared memory segments.
+ /// Includes entries about mappedAtCreation, MAP_READ and MAP_WRITE buffers,
+ /// regardless of their state.
+ std::unordered_map<uint64_t, BufferMapData> mSharedMemoryMap;
+ /// Associated presentation data for each swapchain.
+ std::unordered_map<layers::RemoteTextureOwnerId, RefPtr<PresentationData>,
+ layers::RemoteTextureOwnerId::HashFn>
+ mCanvasMap;
+
+ RefPtr<layers::RemoteTextureOwnerClient> mRemoteTextureOwner;
+
+ /// Associated stack of error scopes for each device.
+ std::unordered_map<uint64_t, ErrorScopeStack> mErrorScopeMap;
+};
+
+} // namespace webgpu
+} // namespace mozilla
+
+#endif // WEBGPU_PARENT_H_
diff --git a/dom/webgpu/ipc/WebGPUSerialize.h b/dom/webgpu/ipc/WebGPUSerialize.h
new file mode 100644
index 0000000000..b130fc992e
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUSerialize.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBGPU_SERIALIZE_H_
+#define WEBGPU_SERIALIZE_H_
+
+#include "WebGPUTypes.h"
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/webgpu/ffi/wgpu.h"
+
+namespace IPC {
+
+#define DEFINE_IPC_SERIALIZER_ENUM_GUARD(something, guard) \
+ template <> \
+ struct ParamTraits<something> \
+ : public ContiguousEnumSerializer<something, something(0), guard> {}
+
+#define DEFINE_IPC_SERIALIZER_DOM_ENUM(something) \
+ DEFINE_IPC_SERIALIZER_ENUM_GUARD(something, something::EndGuard_)
+#define DEFINE_IPC_SERIALIZER_FFI_ENUM(something) \
+ DEFINE_IPC_SERIALIZER_ENUM_GUARD(something, something##_Sentinel)
+
+DEFINE_IPC_SERIALIZER_DOM_ENUM(mozilla::dom::GPUPowerPreference);
+
+DEFINE_IPC_SERIALIZER_FFI_ENUM(mozilla::webgpu::ffi::WGPUHostMap);
+
+DEFINE_IPC_SERIALIZER_WITHOUT_FIELDS(mozilla::dom::GPUCommandBufferDescriptor);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::GPURequestAdapterOptions,
+ mPowerPreference, mForceFallbackAdapter);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::GPUBufferDescriptor, mSize,
+ mUsage, mMappedAtCreation);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::webgpu::ScopedError, operationError,
+ validationMessage);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::webgpu::WebGPUCompilationMessage,
+ message, lineNum, linePos);
+
+#undef DEFINE_IPC_SERIALIZER_FFI_ENUM
+#undef DEFINE_IPC_SERIALIZER_DOM_ENUM
+#undef DEFINE_IPC_SERIALIZER_ENUM_GUARD
+
+} // namespace IPC
+#endif // WEBGPU_SERIALIZE_H_
diff --git a/dom/webgpu/ipc/WebGPUTypes.h b/dom/webgpu/ipc/WebGPUTypes.h
new file mode 100644
index 0000000000..e607e03b99
--- /dev/null
+++ b/dom/webgpu/ipc/WebGPUTypes.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBGPU_TYPES_H_
+#define WEBGPU_TYPES_H_
+
+#include <cstdint>
+#include "mozilla/Maybe.h"
+#include "nsString.h"
+#include "mozilla/dom/BindingDeclarations.h"
+
+namespace mozilla::webgpu {
+
+using RawId = uint64_t;
+using BufferAddress = uint64_t;
+
+struct ScopedError {
+ // Did an error occur as a result the attempt to retrieve an error
+ // (e.g. from a dead device, from an empty scope stack)?
+ bool operationError = false;
+
+ // If non-empty, the first error generated when this scope was on
+ // the top of the stack. This is interpreted as UTF-8.
+ nsCString validationMessage;
+};
+using MaybeScopedError = Maybe<ScopedError>;
+
+enum class WebGPUCompilationMessageType { Error, Warning, Info };
+
+// TODO: Better name? CompilationMessage alread taken by the dom object.
+/// The serializable counterpart of the dom object CompilationMessage.
+struct WebGPUCompilationMessage {
+ nsString message;
+ uint64_t lineNum = 0;
+ uint64_t linePos = 0;
+ // In utf16 code units.
+ uint64_t offset = 0;
+ // In utf16 code units.
+ uint64_t length = 0;
+ WebGPUCompilationMessageType messageType =
+ WebGPUCompilationMessageType::Error;
+};
+
+/// A helper to reduce the boiler plate of turning the many Optional<nsAString>
+/// we get from the dom to the nullable nsACString* we pass to the wgpu ffi.
+class StringHelper {
+ public:
+ explicit StringHelper(const dom::Optional<nsString>& aWide) {
+ if (aWide.WasPassed()) {
+ mNarrow = Some(NS_ConvertUTF16toUTF8(aWide.Value()));
+ }
+ }
+
+ const nsACString* Get() const {
+ if (mNarrow.isSome()) {
+ return mNarrow.ptr();
+ }
+ return nullptr;
+ }
+
+ private:
+ Maybe<NS_ConvertUTF16toUTF8> mNarrow;
+};
+
+} // namespace mozilla::webgpu
+
+#endif // WEBGPU_TYPES_H_
diff --git a/dom/webgpu/mochitest/mochitest-no-pref.ini b/dom/webgpu/mochitest/mochitest-no-pref.ini
new file mode 100644
index 0000000000..d4d111e6ee
--- /dev/null
+++ b/dom/webgpu/mochitest/mochitest-no-pref.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+subsuite = webgpu
+run-if = release_or_beta
+
+# Even if the pref were enabled, WebGPU is only available in secure contexts.
+#
+# See spec WebIDL, like this: https://www.w3.org/TR/webgpu/#navigatorgpu
+scheme = https
+
+[test_disabled.html]
diff --git a/dom/webgpu/mochitest/mochitest.ini b/dom/webgpu/mochitest/mochitest.ini
new file mode 100644
index 0000000000..96b2c55ee8
--- /dev/null
+++ b/dom/webgpu/mochitest/mochitest.ini
@@ -0,0 +1,42 @@
+[DEFAULT]
+subsuite = webgpu
+run-if = !release_or_beta
+prefs =
+ dom.webgpu.enabled=true
+ gfx.offscreencanvas.enabled=true
+support-files =
+ worker_wrapper.js
+ test_basic_canvas.worker.js
+ test_submit_render_empty.worker.js
+
+# WebGPU is only available in secure contexts.
+#
+# See spec WebIDL, like this: https://www.w3.org/TR/webgpu/#navigatorgpu
+scheme = https
+
+[test_basic_canvas.worker.html]
+skip-if = true # Bug 1818379 - no webgpu in worker scopes, see bug 1808820
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_buffer_mapping.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_command_buffer_creation.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_device_creation.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_enabled.html]
+[test_error_scope.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_insecure_context.html]
+# This test checks that WebGPU is not available in insecure contexts.
+scheme = http
+[test_queue_copyExternalImageToTexture.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_queue_write.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_submit_compute_empty.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_submit_render_empty.html]
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
+[test_submit_render_empty.worker.html]
+skip-if = true # Bug 1818379 - no webgpu in worker scopes, see bug 1808820
+fail-if = (os == 'linux' && os_version == '18.04') || (os == 'win' && os_version == '6.1') || (os == 'mac')
diff --git a/dom/webgpu/mochitest/test_basic_canvas.worker.html b/dom/webgpu/mochitest/test_basic_canvas.worker.html
new file mode 100644
index 0000000000..a23ee9fc70
--- /dev/null
+++ b/dom/webgpu/mochitest/test_basic_canvas.worker.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="worker_wrapper.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <canvas id="canvas"></canvas>
+ <script>
+ const canvas = document.getElementById("canvas");
+ const offscreen = canvas.transferControlToOffscreen();
+
+ runWorkerTest("test_basic_canvas.worker.js", { offscreen }, [offscreen]);
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_basic_canvas.worker.js b/dom/webgpu/mochitest/test_basic_canvas.worker.js
new file mode 100644
index 0000000000..5bd0434602
--- /dev/null
+++ b/dom/webgpu/mochitest/test_basic_canvas.worker.js
@@ -0,0 +1,32 @@
+self.addEventListener("message", async function (event) {
+ try {
+ const offscreen = event.data.offscreen;
+ const context = offscreen.getContext("webgpu");
+
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+ const swapChainFormat = context.getPreferredFormat(adapter);
+
+ context.configure({
+ device,
+ format: swapChainFormat,
+ size: { width: 100, height: 100, depth: 1 },
+ });
+
+ const texture = context.getCurrentTexture();
+
+ self.postMessage([
+ {
+ value: texture !== undefined,
+ message: "texture !== undefined",
+ },
+ ]);
+ } catch (e) {
+ self.postMessage([
+ {
+ value: false,
+ message: "Unhandled exception " + e,
+ },
+ ]);
+ }
+});
diff --git a/dom/webgpu/mochitest/test_buffer_mapping.html b/dom/webgpu/mochitest/test_buffer_mapping.html
new file mode 100644
index 0000000000..01dfbf893e
--- /dev/null
+++ b/dom/webgpu/mochitest/test_buffer_mapping.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ async function testBody() {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+
+ const bufferRead = device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+ const bufferWrite = device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC,
+ mappedAtCreation: true,
+ });
+ new Float32Array(bufferWrite.getMappedRange()).set([1.0]);
+ bufferWrite.unmap();
+
+ const encoder = device.createCommandEncoder();
+ encoder.copyBufferToBuffer(bufferWrite, 0, bufferRead, 0, 4);
+ device.queue.submit([encoder.finish()]);
+
+ await bufferRead.mapAsync(GPUMapMode.READ);
+
+ try {
+ bufferRead.getMappedRange(0, 5);
+ ok(false, "mapped with size outside buffer should throw");
+ } catch (e) {
+ ok(
+ true,
+ "mapped with size outside buffer should throw OperationError"
+ );
+ }
+
+ try {
+ bufferRead.getMappedRange(4, 1);
+ ok(false, "mapped with offset outside buffer should throw");
+ } catch (e) {
+ ok(
+ true,
+ "mapped with offset outside buffer should throw OperationError"
+ );
+ }
+
+ const data = bufferRead.getMappedRange();
+ is(data.byteLength, 4, "array should be 4 bytes long");
+
+ const value = new Float32Array(data)[0];
+ ok(value == 1.0, "value == 1.0");
+
+ bufferRead.unmap();
+ is(data.byteLength, 0, "array should be detached after explicit unmap");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ testBody()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_command_buffer_creation.html b/dom/webgpu/mochitest/test_command_buffer_creation.html
new file mode 100644
index 0000000000..a92c038afd
--- /dev/null
+++ b/dom/webgpu/mochitest/test_command_buffer_creation.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+ const encoder = device.createCommandEncoder();
+ const command_buffer = encoder.finish();
+ ok(command_buffer !== undefined, "command_buffer !== undefined");
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_device_creation.html b/dom/webgpu/mochitest/test_device_creation.html
new file mode 100644
index 0000000000..678359c323
--- /dev/null
+++ b/dom/webgpu/mochitest/test_device_creation.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const limits = adapter.limits;
+ const features = adapter.features;
+ const device = await adapter.requestDevice();
+ ok(device !== undefined, "device !== undefined");
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_disabled.html b/dom/webgpu/mochitest/test_disabled.html
new file mode 100644
index 0000000000..12eb01e465
--- /dev/null
+++ b/dom/webgpu/mochitest/test_disabled.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ !SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be disabled."
+ );
+ ok(navigator.gpu === undefined, "navigator.gpu === undefined");
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_enabled.html b/dom/webgpu/mochitest/test_enabled.html
new file mode 100644
index 0000000000..318788bf1e
--- /dev/null
+++ b/dom/webgpu/mochitest/test_enabled.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+ ok(navigator.gpu !== undefined, "navigator.gpu !== undefined");
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_error_scope.html b/dom/webgpu/mochitest/test_error_scope.html
new file mode 100644
index 0000000000..2bed5b937b
--- /dev/null
+++ b/dom/webgpu/mochitest/test_error_scope.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+
+ device.pushErrorScope("validation");
+ const buffer = device.createBuffer({ size: 0, usage: 0 });
+ const error = await device.popErrorScope();
+
+ isnot(error, null);
+
+ try {
+ await device.popErrorScope();
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(ex.name == "OperationError", "Should throw an OperationError");
+ }
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_insecure_context.html b/dom/webgpu/mochitest/test_insecure_context.html
new file mode 100644
index 0000000000..dcc4a313b9
--- /dev/null
+++ b/dom/webgpu/mochitest/test_insecure_context.html
@@ -0,0 +1,22 @@
+<!-- This is somewhat redundant with
+ dom/tests/mochitest/general/test_interfaces.js, but I think it's good to
+ have something here as well. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+ ok(!isSecureContext, "test should not run in a secure context");
+ ok(navigator.gpu === undefined, "navigator.gpu === undefined");
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_queue_copyExternalImageToTexture.html b/dom/webgpu/mochitest/test_queue_copyExternalImageToTexture.html
new file mode 100644
index 0000000000..279b4a52b4
--- /dev/null
+++ b/dom/webgpu/mochitest/test_queue_copyExternalImageToTexture.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "WebGPU pref should be enabled."
+ );
+ ok(
+ SpecialPowers.getBoolPref("gfx.offscreencanvas.enabled"),
+ "OffscreenCanvas pref should be enabled."
+ );
+
+ SimpleTest.waitForExplicitFinish();
+
+ function requestAnimationFramePromise() {
+ return new Promise(requestAnimationFrame);
+ }
+
+ function createSourceCanvasWebgl() {
+ const offscreenCanvas = new OffscreenCanvas(200, 200);
+ const gl = offscreenCanvas.getContext("webgl");
+
+ const COLOR_VALUE = 127.0 / 255.0;
+ const ALPHA_VALUE = 127.0 / 255.0;
+
+ gl.enable(gl.SCISSOR_TEST);
+
+ gl.scissor(0, 0, 100, 100);
+ gl.clearColor(COLOR_VALUE, 0.0, 0.0, ALPHA_VALUE);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(100, 0, 100, 100);
+ gl.clearColor(0.0, COLOR_VALUE, 0.0, ALPHA_VALUE);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(0, 100, 100, 100);
+ gl.clearColor(0.0, 0.0, COLOR_VALUE, ALPHA_VALUE);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(100, 100, 100, 100);
+ gl.clearColor(0.0, 0.0, 0.0, ALPHA_VALUE);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ return {
+ source: offscreenCanvas,
+ origin: { x: 0, y: 0 },
+ flipY: true,
+ };
+ }
+
+ function createSourceCanvas2d() {
+ const offscreenCanvas = new OffscreenCanvas(200, 200);
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "rgba(255,0,0,0.498)";
+ context.fillRect(0, 0, 100, 100);
+
+ context.fillStyle = "rgba(0,255,0,0.498)";
+ context.fillRect(100, 0, 100, 100);
+
+ context.fillStyle = "rgba(0,0,255,0.498)";
+ context.fillRect(0, 100, 100, 100);
+
+ context.fillStyle = "rgba(0,0,0,0.498)";
+ context.fillRect(100, 100, 100, 100);
+
+ return {
+ source: offscreenCanvas,
+ origin: { x: 0, y: 0 },
+ flipY: false,
+ };
+ }
+
+ function createSourceImageBitmap() {
+ const sourceCanvas = createSourceCanvas2d();
+ return {
+ source: sourceCanvas.source.transferToImageBitmap(),
+ origin: { x: 0, y: 0 },
+ flipY: false,
+ };
+ }
+
+ async function mapDestTexture(
+ device,
+ source,
+ destFormat,
+ premultiply,
+ copySize
+ ) {
+ const bytesPerRow = 256 * 4; // 256 aligned for 200 pixels
+ const texture = device.createTexture({
+ format: destFormat,
+ size: copySize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ device.queue.copyExternalImageToTexture(
+ source,
+ { texture, premultipliedAlpha: premultiply },
+ copySize
+ );
+
+ const buffer = device.createBuffer({
+ size: 1024 * 200,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+
+ const encoder = device.createCommandEncoder();
+ encoder.copyTextureToBuffer(
+ { texture },
+ { buffer, bytesPerRow },
+ copySize
+ );
+ device.queue.submit([encoder.finish()]);
+
+ await buffer.mapAsync(GPUMapMode.READ);
+ return buffer;
+ }
+
+ async function verifyBuffer(
+ test,
+ device,
+ source,
+ format,
+ premultiply,
+ copyDim,
+ topLeftPixelData
+ ) {
+ try {
+ const buffer = await mapDestTexture(
+ device,
+ source,
+ format,
+ premultiply,
+ copyDim
+ );
+ const arrayBuffer = buffer.getMappedRange();
+ const view = new Uint8Array(arrayBuffer);
+ for (let i = 0; i < topLeftPixelData.length; ++i) {
+ is(
+ view[i],
+ topLeftPixelData[i],
+ test +
+ " " +
+ format +
+ " (" +
+ source.origin.x +
+ "," +
+ source.origin.y +
+ ") channel " +
+ i
+ );
+ }
+ } catch (e) {
+ ok(false, "WebGPU exception: " + e);
+ }
+ }
+
+ async function verifySourceCanvas(test, device, source) {
+ await verifyBuffer(
+ test,
+ device,
+ source,
+ "rgba8unorm",
+ /* premultiply */ true,
+ { width: 200, height: 200 },
+ [127, 0, 0, 127]
+ );
+ await verifyBuffer(
+ test,
+ device,
+ source,
+ "bgra8unorm",
+ /* premultiply */ true,
+ { width: 200, height: 200 },
+ [0, 0, 127, 127]
+ );
+ await verifyBuffer(
+ test,
+ device,
+ source,
+ "rgba8unorm",
+ /* premultiply */ false,
+ { width: 200, height: 200 },
+ [255, 0, 0, 127]
+ );
+ await verifyBuffer(
+ test,
+ device,
+ source,
+ "bgra8unorm",
+ /* premultiply */ false,
+ { width: 200, height: 200 },
+ [0, 0, 255, 127]
+ );
+
+ // The copy is flipped but the origin is relative to the original source data,
+ // so we need to invert for WebGL.
+ const topRightPixelData =
+ test === "webgl" ? [0, 0, 0, 127] : [0, 127, 0, 127];
+ const topRightOrigin = { origin: { x: 100, y: 0 } };
+ await verifyBuffer(
+ test,
+ device,
+ { ...source, ...topRightOrigin },
+ "bgra8unorm",
+ /* premultiply */ true,
+ { width: 100, height: 100 },
+ topRightPixelData
+ );
+
+ const bottomLeftPixelData =
+ test === "webgl" ? [0, 0, 127, 127] : [127, 0, 0, 127];
+ const bottomLeftOrigin = { origin: { x: 0, y: 100 } };
+ await verifyBuffer(
+ test,
+ device,
+ { ...source, ...bottomLeftOrigin },
+ "bgra8unorm",
+ /* premultiply */ true,
+ { width: 100, height: 100 },
+ bottomLeftPixelData
+ );
+ }
+
+ async function writeDestCanvas(source2d, sourceWebgl, sourceImageBitmap) {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+ await verifySourceCanvas("2d", device, source2d);
+ await verifySourceCanvas("imageBitmap", device, sourceImageBitmap);
+ await verifySourceCanvas("webgl", device, sourceWebgl);
+ }
+
+ async function runTest() {
+ try {
+ const source2d = createSourceCanvas2d();
+ const sourceWebgl = createSourceCanvasWebgl();
+ const sourceImageBitmap = createSourceImageBitmap();
+ await requestAnimationFramePromise();
+ await requestAnimationFramePromise();
+ await writeDestCanvas(source2d, sourceWebgl, sourceImageBitmap);
+ } catch (e) {
+ ok(false, "Uncaught exception: " + e);
+ } finally {
+ SimpleTest.finish();
+ }
+ }
+
+ runTest();
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_queue_write.html b/dom/webgpu/mochitest/test_queue_write.html
new file mode 100644
index 0000000000..585c1617cd
--- /dev/null
+++ b/dom/webgpu/mochitest/test_queue_write.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+ const buffer = device.createBuffer({
+ size: 16,
+ usage:
+ GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.VERTEX,
+ });
+ const arrayBuf = new ArrayBuffer(16);
+ new Int32Array(arrayBuf).fill(5);
+ device.queue.writeBuffer(buffer, 0, arrayBuf, 0);
+ const texture = device.createTexture({
+ size: [2, 2, 1],
+ dimension: "2d",
+ format: "rgba8unorm",
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ });
+ device.queue.writeTexture(
+ { texture },
+ arrayBuf,
+ { bytesPerRow: 8 },
+ [2, 2, 1]
+ );
+ // this isn't a process check, we need to read back the contents and verify the writes happened
+ ok(device !== undefined, "");
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_submit_compute_empty.html b/dom/webgpu/mochitest/test_submit_compute_empty.html
new file mode 100644
index 0000000000..82cb9473c5
--- /dev/null
+++ b/dom/webgpu/mochitest/test_submit_compute_empty.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.end();
+ const command_buffer = encoder.finish();
+ device.queue.submit([command_buffer]);
+ ok(command_buffer !== undefined, "command_buffer !== undefined");
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_submit_render_empty.html b/dom/webgpu/mochitest/test_submit_render_empty.html
new file mode 100644
index 0000000000..bac0d1ede7
--- /dev/null
+++ b/dom/webgpu/mochitest/test_submit_render_empty.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "Pref should be enabled."
+ );
+
+ const func = async function () {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+
+ const swapChainFormat = "rgba8unorm";
+ const bundleEncoder = device.createRenderBundleEncoder({
+ colorFormats: [swapChainFormat],
+ });
+ const bundle = bundleEncoder.finish({});
+
+ const texture = device.createTexture({
+ size: { width: 100, height: 100, depth: 1 },
+ format: swapChainFormat,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const view = texture.createView();
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
+ loadOp: "clear",
+ storeOp: "store",
+ },
+ ],
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ const command_buffer = encoder.finish();
+
+ device.queue.submit([command_buffer]);
+ ok(command_buffer !== undefined, "command_buffer !== undefined");
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ func()
+ .catch(e => ok(false, "Unhandled exception " + e))
+ .finally(() => SimpleTest.finish());
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_submit_render_empty.worker.html b/dom/webgpu/mochitest/test_submit_render_empty.worker.html
new file mode 100644
index 0000000000..8db3168be0
--- /dev/null
+++ b/dom/webgpu/mochitest/test_submit_render_empty.worker.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="worker_wrapper.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ runWorkerTest("test_submit_render_empty.worker.js", {}, []);
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/mochitest/test_submit_render_empty.worker.js b/dom/webgpu/mochitest/test_submit_render_empty.worker.js
new file mode 100644
index 0000000000..6183983ff4
--- /dev/null
+++ b/dom/webgpu/mochitest/test_submit_render_empty.worker.js
@@ -0,0 +1,48 @@
+self.addEventListener("message", async function (event) {
+ try {
+ const adapter = await navigator.gpu.requestAdapter();
+ const device = await adapter.requestDevice();
+
+ const swapChainFormat = "rgba8unorm";
+ const bundleEncoder = device.createRenderBundleEncoder({
+ colorFormats: [swapChainFormat],
+ });
+ const bundle = bundleEncoder.finish({});
+
+ const texture = device.createTexture({
+ size: { width: 100, height: 100, depth: 1 },
+ format: swapChainFormat,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const view = texture.createView();
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ loadValue: { r: 0, g: 0, b: 0, a: 0 },
+ storeOp: "store",
+ },
+ ],
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ const command_buffer = encoder.finish();
+
+ device.queue.submit([command_buffer]);
+ self.postMessage([
+ {
+ value: command_buffer !== undefined,
+ message: "command_buffer !== undefined",
+ },
+ ]);
+ } catch (e) {
+ self.postMessage([
+ {
+ value: false,
+ message: "Unhandled exception " + e,
+ },
+ ]);
+ }
+});
diff --git a/dom/webgpu/mochitest/worker_wrapper.js b/dom/webgpu/mochitest/worker_wrapper.js
new file mode 100644
index 0000000000..6f6de9002d
--- /dev/null
+++ b/dom/webgpu/mochitest/worker_wrapper.js
@@ -0,0 +1,33 @@
+ok(
+ SpecialPowers.getBoolPref("dom.webgpu.enabled"),
+ "WebGPU pref should be enabled."
+);
+ok(
+ SpecialPowers.getBoolPref("gfx.offscreencanvas.enabled"),
+ "OffscreenCanvas pref should be enabled."
+);
+SimpleTest.waitForExplicitFinish();
+
+const workerWrapperFunc = async function (worker_path, data, transfer) {
+ const worker = new Worker(worker_path);
+
+ const results = new Promise((resolve, reject) => {
+ worker.addEventListener("message", event => {
+ resolve(event.data);
+ });
+ });
+
+ worker.postMessage(data, transfer);
+ for (const result of await results) {
+ ok(result.value, result.message);
+ }
+};
+
+async function runWorkerTest(worker_path, data, transfer) {
+ try {
+ await workerWrapperFunc(worker_path, data, transfer);
+ } catch (e) {
+ ok(false, "Unhandled exception " + e);
+ }
+ SimpleTest.finish();
+}
diff --git a/dom/webgpu/moz.build b/dom/webgpu/moz.build
new file mode 100644
index 0000000000..cdc3ec3200
--- /dev/null
+++ b/dom/webgpu/moz.build
@@ -0,0 +1,76 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Graphics: WebGPU")
+
+MOCHITEST_MANIFESTS += [
+ "mochitest/mochitest-no-pref.ini",
+ "mochitest/mochitest.ini",
+]
+
+DIRS += []
+
+h_and_cpp = [
+ "Adapter",
+ "BindGroup",
+ "BindGroupLayout",
+ "Buffer",
+ "CanvasContext",
+ "CommandBuffer",
+ "CommandEncoder",
+ "CompilationInfo",
+ "CompilationMessage",
+ "ComputePassEncoder",
+ "ComputePipeline",
+ "Device",
+ "DeviceLostInfo",
+ "Instance",
+ "ObjectModel",
+ "OutOfMemoryError",
+ "PipelineLayout",
+ "QuerySet",
+ "Queue",
+ "RenderBundle",
+ "RenderBundleEncoder",
+ "RenderPassEncoder",
+ "RenderPipeline",
+ "Sampler",
+ "ShaderModule",
+ "SupportedFeatures",
+ "SupportedLimits",
+ "Texture",
+ "TextureView",
+ "Utility",
+ "ValidationError",
+]
+EXPORTS.mozilla.webgpu += [x + ".h" for x in h_and_cpp]
+UNIFIED_SOURCES += [x + ".cpp" for x in h_and_cpp]
+
+IPDL_SOURCES += [
+ "ipc/PWebGPU.ipdl",
+ "ipc/PWebGPUTypes.ipdlh",
+]
+
+EXPORTS.mozilla.webgpu += [
+ "ipc/WebGPUChild.h",
+ "ipc/WebGPUParent.h",
+ "ipc/WebGPUSerialize.h",
+ "ipc/WebGPUTypes.h",
+]
+
+UNIFIED_SOURCES += [
+ "ipc/WebGPUChild.cpp",
+ "ipc/WebGPUParent.cpp",
+]
+
+if CONFIG["CC_TYPE"] in ("clang", "clang-cl"):
+ CXXFLAGS += ["-Werror=implicit-int-conversion"]
+ CXXFLAGS += ["-Werror=switch"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/webgpu/tests/cts/README.md b/dom/webgpu/tests/cts/README.md
new file mode 100644
index 0000000000..283beeb91f
--- /dev/null
+++ b/dom/webgpu/tests/cts/README.md
@@ -0,0 +1,17 @@
+# WebGPU CTS vendor checkout
+
+This directory contains the following:
+
+```sh
+.
+├── README.md # You are here!
+├── arguments.txt # Used by `vendor/`
+├── checkout/ # Our vendored copy of WebGPU CTS
+├── myexpectations.txt # Used by `vendor/`
+└── vendor/ # Rust binary crate for updating `checkout/` and generating WPT tests
+```
+
+## Re-vendoring
+
+You can re-vendor by running the Rust binary crate from its Cargo project root. Change your working
+directory to `vendor/` and invoke `cargo run -- --help` for more details.
diff --git a/dom/webgpu/tests/cts/arguments.txt b/dom/webgpu/tests/cts/arguments.txt
new file mode 100644
index 0000000000..58acc198c5
--- /dev/null
+++ b/dom/webgpu/tests/cts/arguments.txt
@@ -0,0 +1 @@
+?q=
diff --git a/dom/webgpu/tests/cts/checkout/.eslint-resolver.js b/dom/webgpu/tests/cts/checkout/.eslint-resolver.js
new file mode 100644
index 0000000000..e2b0f32d35
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.eslint-resolver.js
@@ -0,0 +1,23 @@
+const path = require('path');
+const resolve = require('resolve')
+
+// Implements the following resolver spec:
+// https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md
+exports.interfaceVersion = 2
+
+exports.resolve = function (source, file, config) {
+ if (resolve.isCore(source)) return { found: true, path: null }
+
+ source = source.replace(/\.js$/, '.ts');
+ try {
+ return {
+ found: true, path: resolve.sync(source, {
+ extensions: [],
+ basedir: path.dirname(path.resolve(file)),
+ ...config,
+ })
+ }
+ } catch (err) {
+ return { found: false }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/.eslintignore b/dom/webgpu/tests/cts/checkout/.eslintignore
new file mode 100644
index 0000000000..a4a42b1266
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.eslintignore
@@ -0,0 +1 @@
+/src/external/*
diff --git a/dom/webgpu/tests/cts/checkout/.eslintrc.json b/dom/webgpu/tests/cts/checkout/.eslintrc.json
new file mode 100644
index 0000000000..2ea6cbab25
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.eslintrc.json
@@ -0,0 +1,127 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": { "project": "./tsconfig.json" },
+ "extends": [
+ "./node_modules/gts",
+ "plugin:import/errors",
+ "plugin:import/warnings",
+ "plugin:import/typescript"
+ ],
+ "env": {
+ "browser": true,
+ "node": true
+ },
+ "plugins": ["node", "ban", "import", "deprecation"],
+ "rules": {
+ // Core rules
+ "linebreak-style": ["warn", "unix"],
+ "no-console": "warn",
+ "no-undef": "off",
+ "no-useless-rename": "warn",
+ "object-shorthand": "warn",
+ "quotes": ["warn", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
+
+ // All test TODOs must be tracked inside file/test descriptions or READMEs.
+ // Comments relating to TODOs in descriptions can be marked with references like "[1]".
+ // TODOs not relating to test coverage can be marked MAINTENANCE_TODO or similar.
+ "no-warning-comments": ["warn", { "terms": ["todo", "fixme", "xxx"], "location": "anywhere" }],
+
+ // Plugin: @typescript-eslint
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/consistent-type-assertions": "warn",
+ // Recommended lints
+ // https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/README.md
+ "@typescript-eslint/adjacent-overload-signatures": "warn",
+ "@typescript-eslint/await-thenable": "warn",
+ "@typescript-eslint/ban-ts-comment": "warn",
+ "@typescript-eslint/no-empty-interface": "warn",
+ "@typescript-eslint/no-explicit-any": "warn",
+ "@typescript-eslint/no-extra-non-null-assertion": "warn",
+ "@typescript-eslint/no-floating-promises": "warn",
+ "@typescript-eslint/no-for-in-array": "warn",
+ "@typescript-eslint/no-misused-new": "warn",
+ "@typescript-eslint/no-namespace": "warn",
+ "@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
+ "@typescript-eslint/no-this-alias": "warn",
+ "@typescript-eslint/no-unnecessary-type-assertion": "warn",
+ "@typescript-eslint/no-unnecessary-type-constraint": "warn",
+ "@typescript-eslint/no-unused-vars": ["warn", { "vars": "all", "args": "none" }],
+ "@typescript-eslint/prefer-as-const": "warn",
+ "@typescript-eslint/prefer-for-of": "warn",
+ "@typescript-eslint/prefer-namespace-keyword": "warn",
+ "@typescript-eslint/restrict-plus-operands": "warn",
+ "@typescript-eslint/triple-slash-reference": "warn",
+ "@typescript-eslint/unbound-method": "warn",
+ // MAINTENANCE_TODO: Try to clean up and enable these recommended lints?
+ //"@typescript-eslint/no-unsafe-argument": "warn",
+ //"@typescript-eslint/no-unsafe-assignment": "warn",
+ //"@typescript-eslint/no-unsafe-call": "warn",
+ //"@typescript-eslint/no-unsafe-member-access": "warn",
+ //"@typescript-eslint/no-unsafe-return": "warn",
+ // Note: These recommended lints are probably not practical to enable.
+ //"@typescript-eslint/no-misused-promises": "warn",
+ //"@typescript-eslint/no-non-null-assertion": "warn",
+ //"@typescript-eslint/no-var-requires": "warn",
+ //"@typescript-eslint/restrict-template-expressions": "warn",
+
+ // Plugin: ban
+ "ban/ban": [
+ "warn",
+ {
+ "name": "setTimeout",
+ "message": "WPT disallows setTimeout; use `common/util/timeout.js`."
+ }
+ ],
+
+ // Plugin: deprecation
+ //"deprecation/deprecation": "warn",
+
+ // Plugin: import
+ "import/order": [
+ "warn",
+ {
+ "groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
+ "newlines-between": "always",
+ "alphabetize": { "order": "asc", "caseInsensitive": false }
+ }
+ ],
+ "import/newline-after-import": ["warn", { "count": 1 }],
+ "import/no-duplicates": "warn",
+ "import/no-restricted-paths": [
+ "error",
+ {
+ "zones": [
+ {
+ "target": "./src/webgpu",
+ "from": "./src/common",
+ "except": ["./framework", "./util"],
+ "message": "Non-framework common/ code imported from webgpu/ suite"
+ },
+ {
+ "target": "./src/unittests",
+ "from": "./src/common",
+ "except": ["./framework", "./util", "./internal"],
+ "message": "Non-framework common/ code imported from unittests/ suite"
+ },
+ {
+ "target": "./src/webgpu",
+ "from": "./src/unittests",
+ "message": "unittests/ suite imported from webgpu/ suite"
+ },
+ {
+ "target": "./src/common",
+ "from": "./src",
+ "except": ["./common", "./external"],
+ "message": "Non common/ code imported from common/"
+ }
+ ]
+ }
+ ]
+ },
+ "settings": {
+ "import/resolver": {
+ "./.eslint-resolver": {}
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/.github/pull_request_template.md b/dom/webgpu/tests/cts/checkout/.github/pull_request_template.md
new file mode 100644
index 0000000000..7fadba0fc3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.github/pull_request_template.md
@@ -0,0 +1,21 @@
+
+
+
+Issue: #<!-- Fill in the issue number here. See docs/intro/life_of.md -->
+
+<hr>
+
+**Requirements for PR author:**
+
+- [ ] All missing test coverage is tracked with "TODO" or `.unimplemented()`.
+- [ ] New helpers are `/** documented */` and new helper files are found in `helper_index.txt`.
+- [ ] Test behaves as expected in a WebGPU implementation. (If not passing, explain above.)
+
+**Requirements for [reviewer sign-off](https://github.com/gpuweb/cts/blob/main/docs/reviews.md):**
+
+- [ ] Tests are properly located in the test tree.
+- [ ] [Test descriptions](https://github.com/gpuweb/cts/blob/main/docs/intro/plans.md) allow a reader to "read only the test plans and evaluate coverage completeness", and accurately reflect the test code.
+- [ ] Tests provide complete coverage (including validation control cases). **Missing coverage MUST be covered by TODOs.**
+- [ ] Helpers and types promote readability and maintainability.
+
+When landing this PR, be sure to make any necessary issue status updates.
diff --git a/dom/webgpu/tests/cts/checkout/.github/workflows/pr.yml b/dom/webgpu/tests/cts/checkout/.github/workflows/pr.yml
new file mode 100644
index 0000000000..5cf3d70996
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.github/workflows/pr.yml
@@ -0,0 +1,28 @@
+name: Pull Request CI
+
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2.3.1
+ with:
+ persist-credentials: false
+ - run: |
+ git fetch origin ${{ github.event.pull_request.head.sha }}
+ git checkout ${{ github.event.pull_request.head.sha }}
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: "15.x"
+ - run: npm ci
+ - run: npm test
+ - run: |
+ mkdir deploy-build/
+ cp -r README.md src standalone out docs deploy-build/
+ - uses: actions/upload-artifact@v2
+ with:
+ name: pr-artifact
+ path: deploy-build/
diff --git a/dom/webgpu/tests/cts/checkout/.github/workflows/push.yml b/dom/webgpu/tests/cts/checkout/.github/workflows/push.yml
new file mode 100644
index 0000000000..5f767661ab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.github/workflows/push.yml
@@ -0,0 +1,26 @@
+name: Push CI
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2.3.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: "15.x"
+ - run: npm ci
+ - run: |
+ npm test
+ mkdir deploy-build/
+ cp -r README.md src standalone out out-wpt docs tools deploy-build/
+ - uses: JamesIves/github-pages-deploy-action@4.1.4
+ with:
+ BRANCH: gh-pages
+ FOLDER: deploy-build
+ CLEAN: true
diff --git a/dom/webgpu/tests/cts/checkout/.github/workflows/workflow.yml b/dom/webgpu/tests/cts/checkout/.github/workflows/workflow.yml
new file mode 100644
index 0000000000..0d475a269e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.github/workflows/workflow.yml
@@ -0,0 +1,80 @@
+name: Workflow CI
+
+on:
+ workflow_run:
+ workflows:
+ - "Pull Request CI"
+ types:
+ - completed
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2.3.1
+ with:
+ persist-credentials: false
+ - run: |
+ PR=$(curl https://api.github.com/search/issues?q=${{ github.event.workflow_run.head_sha }} |
+ grep -Po "(?<=${{ github.event.workflow_run.repository.full_name }}\/pulls\/)\d*" | head -1)
+ echo "PR=$PR" >> $GITHUB_ENV
+ - uses: actions/github-script@v3
+ id: pr-artifact
+ with:
+ github-token: ${{secrets.GITHUB_TOKEN}}
+ result-encoding: string
+ script: |
+ const artifacts_url = context.payload.workflow_run.artifacts_url
+ const artifacts_req = await github.request(artifacts_url)
+ const artifact = artifacts_req.data.artifacts[0]
+ const download = await github.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: artifact.id,
+ archive_format: "zip"
+ })
+ return download.url
+ - run: |
+ rm -rf *
+ curl -L -o "pr-artifact.zip" "${{ steps.pr-artifact.outputs.result }}"
+ unzip -o pr-artifact.zip
+ rm pr-artifact.zip
+ - run: |
+ cat << EOF >> firebase.json
+ {
+ "hosting": {
+ "public": ".",
+ "ignore": [
+ "firebase.json",
+ "**/.*",
+ "**/node_modules/**"
+ ]
+ }
+ }
+ EOF
+ cat << EOF >> .firebaserc
+ {
+ "projects": {
+ "default": "gpuweb-cts"
+ }
+ }
+ EOF
+ - id: deployment
+ continue-on-error: true
+ uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CTS }}
+ expires: 10d
+ channelId: cts-prs-${{ env.PR }}-${{ github.event.workflow_run.head_sha }}
+ - uses: peter-evans/create-or-update-comment@v1
+ continue-on-error: true
+ if: ${{ steps.deployment.outcome == 'success' }}
+ with:
+ issue-number: ${{ env.PR }}
+ body: |
+ Previews, as seen when this [build job](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}) started (${{ github.event.workflow_run.head_sha }}):
+ [**Run tests**](${{ steps.deployment.outputs.details_url }}/standalone/) | [**View tsdoc**](${{ steps.deployment.outputs.details_url }}/docs/tsdoc/)
+ <!--
+ pr;head;sha
+ ${{ env.PR }};${{ github.event.workflow_run.head_repository.full_name }};${{ github.event.workflow_run.head_sha }}
+ -->
diff --git a/dom/webgpu/tests/cts/checkout/.gitignore b/dom/webgpu/tests/cts/checkout/.gitignore
new file mode 100644
index 0000000000..f115ad4f69
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/.gitignore
@@ -0,0 +1,196 @@
+# VSCode - see .vscode/README.md
+.vscode/
+
+# Build files
+/out/
+/out-wpt/
+/out-node/
+/out-wpt-reftest-screenshots/
+.tscache/
+*.tmp.txt
+/docs/tsdoc/
+
+# Cache files
+/standalone/data
+
+# Created by https://www.gitignore.io/api/linux,macos,windows,node
+# Edit at https://www.gitignore.io/?templates=linux,macos,windows,node
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# rollup.js default build output
+dist/
+
+# Uncomment the public line if your project uses Gatsby
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
+# public
+
+# Storybook build outputs
+.out
+.storybook-out
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Temporary folders
+tmp/
+temp/
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+trace/
+
+# End of https://www.gitignore.io/api/linux,macos,windows,node
diff --git a/dom/webgpu/tests/cts/checkout/CONTRIBUTING.md b/dom/webgpu/tests/cts/checkout/CONTRIBUTING.md
new file mode 100644
index 0000000000..50eb83267b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# GPU for the Web
+
+This repository is being used for work in the [W3C GPU for the Web Community
+Group](https://www.w3.org/community/gpu/), governed by the [W3C Community
+License Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To
+make substantive contributions, you must join the CG.
+
+Contributions to the source code repository are subject to the terms of the
+[3-Clause BSD License](./LICENSE.txt).
+**Contributions will also be exported to
+[web-platform-tests](https://github.com/web-platform-tests/wpt)
+under the same license, and under the terms of its
+[CONTRIBUTING.md](https://github.com/web-platform-tests/wpt/blob/master/CONTRIBUTING.md).**
+
+If you are not the sole contributor to a contribution (pull request), please identify all
+contributors in the pull request comment.
+
+To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
+
+```
++@github_username
+```
+
+If you added a contributor by mistake, you can remove them in a comment with:
+
+```
+-@github_username
+```
+
+If you are making a pull request on behalf of someone else but you had no part in designing the
+feature, you can remove yourself with the above syntax.
diff --git a/dom/webgpu/tests/cts/checkout/Gruntfile.js b/dom/webgpu/tests/cts/checkout/Gruntfile.js
new file mode 100644
index 0000000000..bb48aeaac4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/Gruntfile.js
@@ -0,0 +1,229 @@
+/* eslint-disable node/no-unpublished-require */
+/* eslint-disable prettier/prettier */
+/* eslint-disable no-console */
+
+module.exports = function (grunt) {
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+
+ clean: {
+ out: ['out/', 'out-wpt/', 'out-node/'],
+ },
+
+ run: {
+ 'generate-version': {
+ cmd: 'node',
+ args: ['tools/gen_version'],
+ },
+ 'generate-listings': {
+ cmd: 'node',
+ args: ['tools/gen_listings', 'out/', 'src/webgpu', 'src/stress', 'src/manual', 'src/unittests', 'src/demo'],
+ },
+ 'generate-wpt-cts-html': {
+ cmd: 'node',
+ args: ['tools/gen_wpt_cts_html', 'out-wpt/cts.https.html', 'src/common/templates/cts.https.html'],
+ },
+ 'generate-cache': {
+ cmd: 'node',
+ args: ['tools/gen_cache', 'out/data', 'src/webgpu'],
+ },
+ unittest: {
+ cmd: 'node',
+ args: ['tools/run_node', 'unittests:*'],
+ },
+ 'build-out': {
+ cmd: 'node',
+ args: [
+ 'node_modules/@babel/cli/bin/babel',
+ '--extensions=.ts,.js',
+ '--source-maps=true',
+ '--out-dir=out/',
+ 'src/',
+ ],
+ },
+ 'build-out-wpt': {
+ cmd: 'node',
+ args: [
+ 'node_modules/@babel/cli/bin/babel',
+ '--extensions=.ts,.js',
+ '--source-maps=false',
+ '--delete-dir-on-start',
+ '--out-dir=out-wpt/',
+ 'src/',
+ '--only=src/common/framework/',
+ '--only=src/common/runtime/helper/',
+ '--only=src/common/runtime/wpt.ts',
+ '--only=src/webgpu/',
+ // These files will be generated, instead of compiled from TypeScript.
+ '--ignore=src/common/internal/version.ts',
+ '--ignore=src/webgpu/listing.ts',
+ ],
+ },
+ 'build-out-node': {
+ cmd: 'node',
+ args: [
+ 'node_modules/typescript/lib/tsc.js',
+ '--project', 'node.tsconfig.json',
+ '--outDir', 'out-node/',
+ ],
+ },
+ 'copy-assets': {
+ cmd: 'node',
+ args: [
+ 'node_modules/@babel/cli/bin/babel',
+ 'src/resources/',
+ '--out-dir=out/resources/',
+ '--copy-files'
+ ],
+ },
+ 'copy-assets-wpt': {
+ cmd: 'node',
+ args: [
+ 'node_modules/@babel/cli/bin/babel',
+ 'src/resources/',
+ '--out-dir=out-wpt/resources/',
+ '--copy-files'
+ ],
+ },
+ lint: {
+ cmd: 'node',
+ args: ['node_modules/eslint/bin/eslint', 'src/**/*.ts', '--max-warnings=0'],
+ },
+ presubmit: {
+ cmd: 'node',
+ args: ['tools/presubmit'],
+ },
+ fix: {
+ cmd: 'node',
+ args: ['node_modules/eslint/bin/eslint', 'src/**/*.ts', '--fix'],
+ },
+ 'autoformat-out-wpt': {
+ cmd: 'node',
+ args: ['node_modules/prettier/bin-prettier', '--loglevel=warn', '--write', 'out-wpt/**/*.js'],
+ },
+ tsdoc: {
+ cmd: 'node',
+ args: ['node_modules/typedoc/bin/typedoc'],
+ },
+ 'tsdoc-treatWarningsAsErrors': {
+ cmd: 'node',
+ args: ['node_modules/typedoc/bin/typedoc', '--treatWarningsAsErrors'],
+ },
+
+ serve: {
+ cmd: 'node',
+ args: ['node_modules/http-server/bin/http-server', '-p8080', '-a127.0.0.1', '-c-1']
+ }
+ },
+
+ copy: {
+ 'out-wpt-generated': {
+ files: [
+ { expand: true, cwd: 'out', src: 'common/internal/version.js', dest: 'out-wpt/' },
+ { expand: true, cwd: 'out', src: 'webgpu/listing.js', dest: 'out-wpt/' },
+ ],
+ },
+ 'out-wpt-htmlfiles': {
+ files: [
+ { expand: true, cwd: 'src', src: 'webgpu/**/*.html', dest: 'out-wpt/' },
+ ],
+ },
+ },
+
+ ts: {
+ check: {
+ tsconfig: {
+ tsconfig: 'tsconfig.json',
+ passThrough: true,
+ },
+ },
+ },
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-clean');
+ grunt.loadNpmTasks('grunt-contrib-copy');
+ grunt.loadNpmTasks('grunt-run');
+ grunt.loadNpmTasks('grunt-ts');
+
+ const helpMessageTasks = [];
+ function registerTaskAndAddToHelp(name, desc, deps) {
+ grunt.registerTask(name, deps);
+ addExistingTaskToHelp(name, desc);
+ }
+ function addExistingTaskToHelp(name, desc) {
+ helpMessageTasks.push({ name, desc });
+ }
+
+ grunt.registerTask('set-quiet-mode', () => {
+ grunt.log.write('Running tasks');
+ require('quiet-grunt');
+ });
+
+ grunt.registerTask('build-standalone', 'Build out/ (no checks, no WPT)', [
+ 'run:build-out',
+ 'run:copy-assets',
+ 'run:generate-version',
+ 'run:generate-listings',
+ ]);
+ grunt.registerTask('build-wpt', 'Build out/ (no checks)', [
+ 'run:build-out-wpt',
+ 'run:copy-assets-wpt',
+ 'run:autoformat-out-wpt',
+ 'run:generate-version',
+ 'run:generate-listings',
+ 'copy:out-wpt-generated',
+ 'copy:out-wpt-htmlfiles',
+ 'run:generate-wpt-cts-html',
+ ]);
+ grunt.registerTask('build-done-message', () => {
+ process.stderr.write('\nBuild completed! Running checks/tests');
+ });
+
+ registerTaskAndAddToHelp('pre', 'Run all presubmit checks: standalone+wpt+typecheck+unittest+lint', [
+ 'set-quiet-mode',
+ 'clean',
+ 'build-standalone',
+ 'build-wpt',
+ 'run:build-out-node',
+ 'build-done-message',
+ 'ts:check',
+ 'run:presubmit',
+ 'run:unittest',
+ 'run:lint',
+ 'run:tsdoc-treatWarningsAsErrors',
+ ]);
+ registerTaskAndAddToHelp('standalone', 'Build standalone and typecheck', [
+ 'set-quiet-mode',
+ 'build-standalone',
+ 'build-done-message',
+ 'ts:check',
+ ]);
+ registerTaskAndAddToHelp('wpt', 'Build for WPT and typecheck', [
+ 'set-quiet-mode',
+ 'build-wpt',
+ 'build-done-message',
+ 'ts:check',
+ ]);
+ registerTaskAndAddToHelp('unittest', 'Build standalone, typecheck, and unittest', [
+ 'standalone',
+ 'run:unittest',
+ ]);
+ registerTaskAndAddToHelp('check', 'Just typecheck', [
+ 'set-quiet-mode',
+ 'ts:check',
+ ]);
+
+ registerTaskAndAddToHelp('serve', 'Serve out/ on 127.0.0.1:8080 (does NOT compile source)', ['run:serve']);
+ registerTaskAndAddToHelp('fix', 'Fix lint and formatting', ['run:fix']);
+
+ addExistingTaskToHelp('clean', 'Clean out/ and out-wpt/');
+
+ grunt.registerTask('default', '', () => {
+ console.error('\nAvailable tasks (see grunt --help for info):');
+ for (const { name, desc } of helpMessageTasks) {
+ console.error(`$ grunt ${name}`);
+ console.error(` ${desc}`);
+ }
+ });
+};
diff --git a/dom/webgpu/tests/cts/checkout/LICENSE.txt b/dom/webgpu/tests/cts/checkout/LICENSE.txt
new file mode 100644
index 0000000000..c7a75d7d22
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright 2019 WebGPU CTS Contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ 3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/dom/webgpu/tests/cts/checkout/README.md b/dom/webgpu/tests/cts/checkout/README.md
new file mode 100644
index 0000000000..1614f9a979
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/README.md
@@ -0,0 +1,22 @@
+# WebGPU Conformance Test Suite
+
+This is the conformance test suite for WebGPU.
+It tests the behaviors defined by the [WebGPU specification](https://gpuweb.github.io/gpuweb/).
+
+The contents of this test suite are considered **normative**; implementations must pass
+them to be WebGPU-conformant. Mismatches between the specification and tests are bugs.
+
+This test suite can be embedded inside [WPT](https://github.com/web-platform-tests/wpt) or run in standalone.
+
+## [Launch the standalone CTS runner / test plan viewer](https://gpuweb.github.io/cts/standalone/)
+
+## Contributing
+
+Please read the [introductory guidelines](docs/intro/README.md) before contributing.
+Other documentation may be found in [`docs/`](docs/) and in the [helper index](https://gpuweb.github.io/cts/docs/tsdoc/) ([source](docs/helper_index.txt)).
+
+Read [CONTRIBUTING.md](CONTRIBUTING.md) on licensing.
+
+For realtime communication about WebGPU spec and test, join the
+[#WebGPU:matrix.org room](https://app.element.io/#/room/#WebGPU:matrix.org)
+on Matrix.
diff --git a/dom/webgpu/tests/cts/checkout/babel.config.js b/dom/webgpu/tests/cts/checkout/babel.config.js
new file mode 100644
index 0000000000..ad977bc510
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/babel.config.js
@@ -0,0 +1,21 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['@babel/preset-typescript'],
+ plugins: [
+ 'const-enum',
+ [
+ 'add-header-comment',
+ {
+ header: ['AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts'],
+ },
+ ],
+ ],
+ compact: false,
+ // Keeps comments from getting hoisted to the end of the previous line of code.
+ // (Also keeps lines close to their original line numbers - but for WPT we
+ // reformat with prettier anyway.)
+ retainLines: true,
+ shouldPrintComment: val => !/eslint|prettier-ignore/.test(val),
+ };
+};
diff --git a/dom/webgpu/tests/cts/checkout/cts.code-workspace b/dom/webgpu/tests/cts/checkout/cts.code-workspace
new file mode 100644
index 0000000000..aeca61e712
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/cts.code-workspace
@@ -0,0 +1,110 @@
+// Note: VS Code's setting precedence is `.vscode/` > `cts.code-workspace` > global user settings.
+{
+ "folders": [
+ {
+ "name": "cts",
+ "path": "."
+ },
+ {
+ "name": "webgpu",
+ "path": "src/webgpu"
+ }
+ ],
+ "settings": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.detectIndentation": false,
+ "editor.rulers": [100],
+ "editor.tabSize": 2,
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "files.trimTrailingWhitespace": true,
+ "files.exclude": {
+ "*.tmp.txt": true,
+ ".gitignore": true,
+ ".travis.yml": true,
+ ".tscache": true,
+ "deploy_key.enc": true,
+ "node_modules": true,
+ "out": true,
+ "out-node": true,
+ "out-wpt": true,
+ "docs/tsdoc": true,
+ "package-lock.json": true
+ },
+ // Configure VSCode to use the right style when automatically adding imports on autocomplete.
+ "typescript.preferences.importModuleSpecifier": "relative",
+ "typescript.preferences.importModuleSpecifierEnding": "js",
+ "typescript.preferences.quoteStyle": "single"
+ },
+ "tasks": {
+ "version": "2.0.0",
+ "tasks": [
+ // Only supports "shell" and "process" tasks.
+ // https://code.visualstudio.com/docs/editor/multi-root-workspaces#_workspace-task-configuration
+ {
+ // Use "group": "build" instead of "test" so it's easy to access from cmd-shift-B.
+ "group": "build",
+ "label": "npm: test",
+ "detail": "Run all presubmit checks",
+
+ "type": "shell",
+ "command": "npm run test",
+ "problemMatcher": []
+ },
+ {
+ "group": "build",
+ "label": "npm: check",
+ "detail": "Just typecheck",
+
+ "type": "shell",
+ "command": "npm run check",
+ "problemMatcher": ["$tsc"]
+ },
+ {
+ "group": "build",
+ "label": "npm: standalone",
+ "detail": "Build standalone and typecheck",
+
+ "type": "shell",
+ "command": "npm run standalone",
+ "problemMatcher": []
+ },
+ {
+ "group": "build",
+ "label": "npm: wpt",
+ "detail": "Build for WPT and typecheck",
+
+ "type": "shell",
+ "command": "npm run wpt",
+ "problemMatcher": []
+ },
+ {
+ "group": "build",
+ "label": "npm: unittest",
+ "detail": "Build standalone, typecheck, and unittest",
+
+ "type": "shell",
+ "command": "npm run unittest",
+ "problemMatcher": []
+ },
+ {
+ "group": "build",
+ "label": "npm: tsdoc",
+ "detail": "Build docs/tsdoc/",
+
+ "type": "shell",
+ "command": "npm run tsdoc",
+ "problemMatcher": []
+ },
+ {
+ "group": "build",
+ "label": "grunt: run:lint",
+ "detail": "Run eslint",
+
+ "type": "shell",
+ "command": "npx grunt run:lint",
+ "problemMatcher": ["$eslint-stylish"]
+ },
+ ]
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/docs/build.md b/dom/webgpu/tests/cts/checkout/docs/build.md
new file mode 100644
index 0000000000..2d7b2f968c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/build.md
@@ -0,0 +1,43 @@
+# Building
+
+Building the project is not usually needed for local development.
+However, for exports to WPT, or deployment (https://gpuweb.github.io/cts/),
+files can be pre-generated.
+
+The project builds into two directories:
+
+- `out/`: Built framework and test files, needed to run standalone or command line.
+- `out-wpt/`: Build directory for export into WPT. Contains:
+ - An adapter for running WebGPU CTS tests under WPT
+ - A copy of the needed files from `out/`
+ - A copy of any `.html` test cases from `src/`
+
+To build and run all pre-submit checks (including type and lint checks and
+unittests), use:
+
+```sh
+npm test
+```
+
+For checks only:
+
+```sh
+npm run check
+```
+
+For a quicker iterative build:
+
+```sh
+npm run standalone
+```
+
+## Run
+
+To serve the built files (rather than using the dev server), run `npx grunt serve`.
+
+## Export to WPT
+
+Run `npm run wpt`.
+
+Copy (or symlink) the `out-wpt/` directory as the `webgpu/` directory in your
+WPT checkout or your browser's "internal" WPT test directory.
diff --git a/dom/webgpu/tests/cts/checkout/docs/deno.md b/dom/webgpu/tests/cts/checkout/docs/deno.md
new file mode 100644
index 0000000000..22a54c79bd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/deno.md
@@ -0,0 +1,24 @@
+# Running the CTS on Deno
+
+Since version 1.8, Deno experimentally implements the WebGPU API out of the box.
+You can use the `./tools/deno` script to run the CTS in Deno. To do this you
+will first need to install Deno: [stable](https://deno.land#installation), or
+build the main branch from source
+(`cargo install --git https://github.com/denoland/deno --bin deno`).
+
+On macOS and recent Linux, you can just run `./tools/run_deno` as is. On Windows and
+older Linux releases you will need to run
+`deno run --unstable --allow-read --allow-write --allow-env ./tools/deno`.
+
+## Usage
+
+```
+Usage:
+ tools/run_deno [OPTIONS...] QUERIES...
+ tools/run_deno 'unittests:*' 'webgpu:buffers,*'
+Options:
+ --verbose Print result/log of every test as it runs.
+ --debug Include debug messages in logging.
+ --print-json Print the complete result JSON in the output.
+ --expectations Path to expectations file.
+```
diff --git a/dom/webgpu/tests/cts/checkout/docs/fp_primer.md b/dom/webgpu/tests/cts/checkout/docs/fp_primer.md
new file mode 100644
index 0000000000..234a43de40
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/fp_primer.md
@@ -0,0 +1,516 @@
+# Floating Point Primer
+
+This document is meant to be a primer of the concepts related to floating point
+numbers that are needed to be understood when working on tests in WebGPU's CTS.
+
+WebGPU's CTS is responsible for testing if implementations of WebGPU are
+conformant to the spec, and thus interoperable with each other.
+
+Floating point math makes up a significant portion of the WGSL spec, and has
+many subtle corner cases to get correct.
+
+Additionally, floating point math, unlike integer math, is broadly not exact, so
+how inaccurate a calculation is allowed to be is required to be stated in the
+spec and tested in the CTS, as opposed to testing for a singular correct
+response.
+
+Thus, the WebGPU CTS has a significant amount of machinery around how to
+correctly test floating point expectations in a fluent manner.
+
+## Floating Point Numbers
+
+For the context of this discussion floating point numbers, fp for short, are
+single precision IEEE floating point numbers, f32 for short.
+
+Details of how this format works are discussed as needed below, but for a more
+involved discussion, please see the references in the Resources sections.
+
+Additionally, in the Appendix there is a table of interesting/common values that
+are often referenced in tests or this document.
+
+*In the future support for f16 and abstract floats will be added to the CTS, and
+this document will need to be updated.*
+
+Floating point numbers are effectively lossy compression of the infinite number
+of possible values over their range down to 32-bits of distinct points.
+
+This means that not all numbers in the range can be exactly represented as a f32.
+
+For example, the integer `1` is exactly represented as `0x3f800000`, but the next
+nearest number `0x3f800001` is `1.00000011920928955`.
+
+So any number between `1` and `1.00000011920928955` is not exactly represented
+as a f32 and instead is approximated as either `1` or `1.00000011920928955`.
+
+When a number X is not exactly represented by a f32 value, there are normally
+two neighbouring numbers that could reasonably represent X: the nearest f32
+value above X, and the nearest f32 value below X. Which of these values gets
+used is dictated by the rounding mode being used, which may be something like
+always round towards 0 or go to the nearest neighbour, or something else
+entirely.
+
+The process of converting numbers between precisions, like non-f32 to f32, is
+called quantization. WGSL does not prescribe a specific rounding mode when
+quantizing, so either of the neighbouring values is considered valid
+when converting a non-exactly representable value to f32. This has significant
+implications on the CTS that are discussed later.
+
+From here on, we assume you are familiar with the internal structure of a f32
+value: a sign bit, a biased exponent, and a mantissa. For reference, see
+[float32 on Wikipedia](https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
+
+In the f32 format as described above, there are two possible zero values, one
+with all bits being 0, called positive zero, and one all the same except with
+the sign bit being 1, called negative zero.
+
+For WGSL, and thus the CTS's purposes, these values are considered equivalent.
+Typescript, which the CTS is written in, treats all zeros as positive zeros,
+unless you explicitly escape hatch to differentiate between them, so most of the
+time there being two zeros doesn't materially affect code.
+
+### Normals
+
+Normal numbers are floating point numbers whose biased exponent is not all 0s or
+all 1s. For WGSL these numbers behave as you expect for floating point values
+with no interesting caveats.
+
+### Subnormals
+
+Subnormal numbers are numbers whose biased exponent is all 0s, also called
+denorms.
+
+These are the closest numbers to zero, both positive and negative, and fill in
+the gap between the normal numbers with smallest magnitude, and 0.
+
+Some devices, for performance reasons, do not handle operations on the
+subnormal numbers, and instead treat them as being zero, this is called *flush
+to zero* or FTZ behaviour.
+
+This means in the CTS that when a subnormal number is consumed or produced by an
+operation, an implementation may choose to replace it with zero.
+
+Like the rounding mode for quantization, this adds significant complexity to the
+CTS, which will be discussed later.
+
+### Inf & NaNs
+
+Floating point numbers include positive and negative infinity to represent
+values that are out of the bounds supported by the current precision.
+
+Implementations may assume that infinities are not present. When an evaluation
+would produce an infinity, an undefined value is produced instead.
+
+Additionally, when a calculation would produce a finite value outside the
+bounds of the current precision, the implementation may convert that value to
+either an infinity with same sign, or the min/max representable value as
+appropriate.
+
+The CTS encodes the least restrictive interpretation of the rules in the spec,
+i.e. assuming someone has made a slightly adversarial implementation that always
+chooses the thing with the least accuracy.
+
+This means that the above rules about infinities combine to say that any time an
+out of bounds value is seen, any finite value is acceptable afterwards.
+
+This is because the out of bounds value may be converted to an infinity and then
+an undefined value can be used instead of the infinity.
+
+This is actually a significant boon for the CTS implementation, because it short
+circuits a bunch of complexity about clamping to edge values and handling
+infinities.
+
+Signaling NaNs are treated as quiet NaNs in the WGSL spec. And quiet NaNs have
+the same "may-convert-to-undefined-value" behaviour that infinities have, so for
+the purpose of the CTS they are handled by the infinite/out of bounds logic
+normally.
+
+## Notation/Terminology
+
+When discussing floating point values in the CTS, there are a few terms used
+with precise meanings, which will be elaborated here.
+
+Additionally, any specific notation used will be specified here to avoid
+confusion.
+
+### Operations
+
+The CTS tests for the proper execution of f32 builtins, i.e. sin, sqrt, abs,
+etc, and expressions, i.e. *, /, <, etc. These collectively can be referred to
+as f32 operations.
+
+Operations, which can be thought of as mathematical functions, are mappings from
+a set of inputs to a set of outputs.
+
+Denoted `f(x, y) = X`, where f is a placeholder or the name of the operation,
+lower case variables are the inputs to the function, and uppercase variables are
+the outputs of the function.
+
+Operations have one or more inputs and an output. Being a f32 operation means
+that the primary space for input and output values is f32, but there is some
+flexibility in this definition. For example operations with values being
+restricted to a subset of integers that are representable as f32 are often
+referred to as being f32 based.
+
+Values are generally floats, integers, booleans, vector, and matrices. Consult
+the WGSL spec for the exact list of types and their definitions.
+
+For composite outputs where there are multiple values being returned, there is a
+single result value made of structured data. Whereas inputs handle this by
+having multiple input parameters.
+
+Some examples of different types of operations:
+
+`multiplication(x, y) = X`, which represents the WGSL expression `x * y`, takes
+in f32 values, `x` and `y`, and produces a f32 value `X`.
+
+`lessThen(x, y) = X`, which represents the WGSL expression `x < y`, again takes
+in f32 values, but in this case returns a boolean value.
+
+`ldexp(x, y) = X`, which builds a f32 takes, takes in a f32 values `x` and a
+restricted integer `y`.
+
+### Domain, Range, and Intervals
+
+For an operation `f(x) = X`, the interval of valid values for the input, `x`, is
+called the *domain*, and the interval for valid results, `X`, is called the
+*range*.
+
+An interval, `[a, b]`, is a set of real numbers that contains `a`, `b`, and all
+the real numbers between them.
+
+Open-ended intervals, i.e. ones that don't include `a` and/or `b`, are avoided,
+and are called out explicitly when they occur.
+
+The convention in this doc and the CTS code is that `a <= b`, so `a` can be
+referred to as the beginning of the interval and `b` as the end of the interval.
+
+When talking about intervals, this doc and the code endeavours to avoid using
+the term **range** to refer to the span of values that an interval covers,
+instead using the term bounds to avoid confusion of terminology around output of
+operations.
+
+## Accuracy
+
+As mentioned above floating point numbers are not able to represent all the
+possible values over their bounds, but instead represent discrete values in that
+interval, and approximate the remainder.
+
+Additionally, floating point numbers are not evenly distributed over the real
+number line, but instead are clustered closer together near zero, and further
+apart as their magnitudes grow.
+
+When discussing operations on floating point numbers, there is often reference
+to a true value. This is the value that given no performance constraints and
+infinite precision you would get, i.e `acos(1) = π`, where π has infinite
+digits of precision.
+
+For the CTS it is often sufficient to calculate the true value using TypeScript,
+since its native number format is higher precision (double-precision/f64), and
+all f32 values can be represented in it.
+
+The true value is sometimes representable exactly as a f32 value, but often is
+not.
+
+Additionally, many operations are implemented using approximations from
+numerical analysis, where there is a tradeoff between the precision of the
+result and the cost.
+
+Thus, the spec specifies what the accuracy constraints for specific operations
+is, how close to truth an implementation is required to be, to be
+considered conformant.
+
+There are 5 different ways that accuracy requirements are defined in the spec:
+
+1. *Exact*
+
+ This is the situation where it is expected that true value for an operation
+ is always expected to be exactly representable. This doesn't happen for any
+ of the operations that return floating point values, but does occur for
+ logical operations that return boolean values.
+
+
+2. *Correctly Rounded*
+
+ For the case that the true value is exactly representable as a f32, this is
+ the equivalent of exactly from above. In the event that the true value is not
+ exact, then the acceptable answer for most numbers is either the nearest f32
+ above or the nearest f32 below the true value.
+
+ For values near the subnormal range, e.g. close to zero, this becomes more
+ complex, since an implementation may FTZ at any point. So if the exact
+ solution is subnormal or either of the neighbours of the true value are
+ subnormal, zero becomes a possible result, thus the acceptance interval is
+ wider than naively expected.
+
+
+3. *Absolute Error*
+
+ This type of accuracy specifies an error value, ε, and the calculated result
+ is expected to be within that distance from the true value, i.e.
+ `[ X - ε, X + ε ]`.
+
+ The main drawback with this manner of specifying accuracy is that it doesn't
+ scale with the level of precision in floating point numbers themselves at a
+ specific value. Thus, it tends to be only used for specifying accuracy over
+ specific limited intervals, i.e. [-π, π].
+
+
+4. *Units of Least Precision (ULP)*
+
+ The solution to the issue of not scaling with precision of floating point is
+ to use units of least precision.
+
+ ULP(X) is min (b-a) over all pairs (a,b) of representable floating point
+ numbers such that (a <= X <= b and a =/= b). For a more formal discussion of
+ ULP see
+ [On the definition of ulp(x)](https://hal.inria.fr/inria-00070503/document).
+
+ n * ULP or nULP means `[X - n * ULP @ X, X + n * ULP @ X]`.
+
+
+5. *Inherited*
+
+ When an operation's accuracy is defined in terms of other operations, then
+ its accuracy is said to be inherited. Handling of inherited accuracies is
+ one of the main driving factors in the design of testing framework, so will
+ need to be discussed in detail.
+
+## Acceptance Intervals
+
+The first four accuracy types; Exact, Correctly Rounded, Absolute Error, and
+ULP, sometimes called simple accuracies, can be defined in isolation from each
+other, and by association can be implemented using relatively independent
+implementations.
+
+The original implementation of the floating point framework did this as it was
+being built out, but ran into difficulties when defining the inherited
+accuracies.
+
+For examples, `tan(x) inherits from sin(x)/cos(x)`, one can take the defined
+rules and manually build up a bespoke solution for checking the results, but
+this is tedious, error-prone, and doesn't allow for code re-use.
+
+Instead, it would be better if there was a single conceptual framework that one
+can express all the 'simple' accuracy requirements in, and then have a mechanism
+for composing them to define inherited accuracies.
+
+In the WebGPU CTS this is done via the concept of acceptance intervals, which is
+derived from a similar concept in the Vulkan CTS, though implemented
+significantly differently.
+
+The core of this idea is that each of different accuracy types can be integrated
+into the definition of the operation, so that instead of transforming an input
+from the domain to a point in the range, the operation is producing an interval
+in the range, that is the acceptable values an implementation may emit.
+
+
+The simple accuracies can be defined as follows:
+
+1. *Exact*
+
+ `f(x) => [X, X]`
+
+
+2. *Correctly Rounded*
+
+ If `X` is precisely defined as a f32
+
+ `f(x) => [X, X]`
+
+ otherwise,
+
+ `[a, b]` where `a` is the largest representable number with `a <= X`, and `b`
+ is the smallest representable number with `X <= b`
+
+
+3. *Absolute Error*
+
+ `f(x) => [ X - ε, X + ε ]`, where ε is the absolute error value
+
+
+4. **ULP Error**
+
+ `f(x) = X => [X - n*ULP(X), X + n*ULP(X)]`
+
+As defined, these definitions handle mapping from a point in the domain into an
+interval in the range.
+
+This is insufficient for implementing inherited accuracies, since inheritance
+sometimes involve mapping domain intervals to range intervals.
+
+Here we use the convention for naturally extending a function on real numbers
+into a function on intervals of real numbers, i.e. `f([a, b]) = [A, B]`.
+
+Given that floating point numbers have a finite number of precise values for any
+given interval, one could implement just running the accuracy computation for
+every point in the interval and then spanning together the resultant intervals.
+That would be very inefficient though and make your reviewer sad to read.
+
+For mapping intervals to intervals the key insight is that we only need to be
+concerned with the extrema of the operation in the interval, since the
+acceptance interval is the bounds of the possible outputs.
+
+In more precise terms:
+```
+ f(x) => X, x = [a, b] and X = [A, B]
+
+ X = [min(f(x)), max(f(x))]
+ X = [min(f([a, b])), max(f([a, b]))]
+ X = [f(m), f(M)]
+```
+where m and M are in `[a, b]`, `m <= M`, and produce the min and max results
+for `f` on the interval, respectively.
+
+So how do we find the minima and maxima for our operation in the domain?
+
+The common general solution for this requires using calculus to calculate the
+derivative of `f`, `f'`, and then find the zeroes `f'` to find inflection
+points of `f`.
+
+This solution wouldn't be sufficient for all builtins, i.e. `step` which is not
+differentiable at 'edge' values.
+
+Thankfully we do not need a general solution for the CTS, since all the builtin
+operations are defined in the spec, so `f` is from a known set of options.
+
+These operations can be divided into two broad categories: monotonic, and
+non-monotonic, with respect to an interval.
+
+The monotonic operations are ones that preserve the order of inputs in their
+outputs (or reverse it). Their graph only ever decreases or increases,
+never changing from one or the other, though it can have flat sections.
+
+The non-monotonic operations are ones whose graph would have both regions of
+increase and decrease.
+
+The monotonic operations, when mapping an interval to an interval, are simple to
+handle, since the extrema are guaranteed to be the ends of the domain, `a` and `b`.
+
+So `f([a, b])` = `[f(a), f(b)]` or `[f(b), f(a)]`. We could figure out if `f` is
+increasing or decreasing beforehand to determine if it should be `[f(a), f(b)]`
+or `[f(b), f(a)]`.
+
+It is simpler to just use min & max to have an implementation that is agnostic
+to the details of `f`.
+```
+ A = f(a), B = f(b)
+ X = [min(A, B), max(A, B)]
+```
+
+The non-monotonic functions that we need to handle for interval-to-interval
+mappings are more complex. Thankfully are a small number of the overall
+operations that need to be handled, since they are only the operations that are
+used in an inherited accuracy and take in the output of another operation as
+part of that inherited accuracy.
+
+So in the CTS we just have bespoke implementations for each of them.
+
+Part of the operation definition in the CTS is a function that takes in the
+domain interval, and returns a sub-interval such that the subject function is
+monotonic over that sub-interval, and hence the function's minima and maxima are
+at the ends.
+
+This adjusted domain interval can then be fed through the same machinery as the
+monotonic functions.
+
+### Inherited Accuracy
+
+So with all of that background out of the way, we can now define an inherited
+accuracy in terms of acceptance intervals.
+
+The crux of this is the insight that the range of one operation can become the
+domain of another operation to compose them together.
+
+And since we have defined how to do this interval to interval mapping above,
+transforming things becomes mechanical and thus implementable in reusable code.
+
+When talking about inherited accuracies `f(x) => g(x)` is used to denote that
+`f`'s accuracy is a defined as `g`.
+
+An example to illustrate inherited accuracies:
+
+```
+ tan(x) => sin(x)/cos(x)
+
+ sin(x) => [sin(x) - 2^-11, sin(x) + 2^-11]`
+ cos(x) => [cos(x) - 2^-11, cos(x) + 2-11]
+
+ x/y => [x/y - 2.5 * ULP(x/y), x/y + 2.5 * ULP(x/y)]
+```
+
+`sin(x)` and `cos(x)` are non-monotonic, so calculating out a closed generic
+form over an interval is a pain, since the min and max vary depending on the
+value of x. Let's isolate this to a single point, so you don't have to read
+literally pages of expanded intervals.
+
+```
+ x = π/2
+
+ sin(π/2) => [sin(π/2) - 2-11, sin(π/2) + 2-11]
+ => [0 - 2-11, 0 + 2-11]
+ => [-0.000488.., 0.000488...]
+ cos(π/2) => [cos(π/2) - 2-11, cos(π/2) + 2-11]
+ => [-0.500488, -0.499511...]
+
+ tan(π/2) => sin(π/2)/cos(π/2)
+ => [-0.000488.., 0.000488...]/[-0.500488..., -0.499511...]
+ => [min({-0.000488.../-0.500488..., -0.000488.../-0.499511..., ...}),
+ max(min({-0.000488.../-0.500488..., -0.000488.../-0.499511..., ...}) ]
+ => [0.000488.../-0.499511..., 0.000488.../0.499511...]
+ => [-0.0009775171, 0.0009775171]
+```
+
+For clarity this has omitted a bunch of complexity around FTZ behaviours, and
+that these operations are only defined for specific domains, but the high-level
+concepts hold.
+
+For each of the inherited operations we could implement a manually written out
+closed form solution, but that would be quite error-prone and not be
+re-using code between builtins.
+
+Instead, the CTS takes advantage of the fact in addition to testing
+implementations of `tan(x)` we are going to be testing implementations of
+`sin(x)`, `cos(x)` and `x/y`, so there should be functions to generate
+acceptance intervals for those operations.
+
+The `tan(x)` acceptance interval can be constructed by generating the acceptance
+intervals for `sin(x)`, `cos(x)` and `x/y` via function calls and composing the
+results.
+
+This algorithmically looks something like this:
+
+```
+ tan(x):
+ Calculate sin(x) interval
+ Calculate cos(x) interval
+ Calculate sin(x) result divided by cos(x) result
+ Return division result
+```
+
+# Appendix
+
+### Significant f32 Values
+
+| Name | Decimal (~) | Hex | Sign Bit | Exponent Bits | Significand Bits |
+| ---------------------- | --------------: | ----------: | -------: | ------------: | ---------------------------: |
+| Negative Infinity | -∞ | 0xff80 0000 | 1 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
+| Min Negative Normal | -3.40282346E38 | 0xff7f ffff | 1 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
+| Max Negative Normal | -1.1754943E−38 | 0x8080 0000 | 1 | 0000 0001 | 0000 0000 0000 0000 0000 000 |
+| Min Negative Subnormal | -1.1754942E-38 | 0x807f ffff | 1 | 0000 0000 | 1111 1111 1111 1111 1111 111 |
+| Max Negative Subnormal | -1.4012984E−45 | 0x8000 0001 | 1 | 0000 0000 | 0000 0000 0000 0000 0000 001 |
+| Negative Zero | -0 | 0x8000 0000 | 1 | 0000 0000 | 0000 0000 0000 0000 0000 000 |
+| Positive Zero | 0 | 0x0000 0000 | 0 | 0000 0000 | 0000 0000 0000 0000 0000 000 |
+| Min Positive Subnormal | 1.4012984E−45 | 0x0000 0001 | 0 | 0000 0000 | 0000 0000 0000 0000 0000 001 |
+| Max Positive Subnormal | 1.1754942E-38 | 0x007f ffff | 0 | 0000 0000 | 1111 1111 1111 1111 1111 111 |
+| Min Positive Normal | 1.1754943E−38 | 0x0080 0000 | 0 | 0000 0001 | 0000 0000 0000 0000 0000 000 |
+| Max Positive Normal | 3.40282346E38 | 0x7f7f ffff | 0 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
+| Negative Infinity | ∞ | 0x7f80 0000 | 0 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
+
+# Resources
+- [WebGPU Spec](https://www.w3.org/TR/webgpu/)
+- [WGSL Spec](https://www.w3.org/TR/WGSL/)
+- [float32 on Wikipedia](https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
+- [IEEE-754 Floating Point Converter](https://www.h-schmidt.net/FloatConverter/IEEE754.html)
+- [IEEE 754 Calculator](http://weitz.de/ieee/)
+- [Keisan High Precision Calculator](https://keisan.casio.com/calculator)
+- [On the definition of ulp(x)](https://hal.inria.fr/inria-00070503/document)
diff --git a/dom/webgpu/tests/cts/checkout/docs/helper_index.txt b/dom/webgpu/tests/cts/checkout/docs/helper_index.txt
new file mode 100644
index 0000000000..1b0a503246
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/helper_index.txt
@@ -0,0 +1,92 @@
+<!--
+ View this file in Typedoc!
+
+ - At https://gpuweb.github.io/cts/docs/tsdoc/
+ - Or locally:
+ - npm run tsdoc
+ - npm start
+ - http://localhost:8080/docs/tsdoc/
+
+ This file is parsed as a tsdoc.
+-->
+
+## Index of Test Helpers
+
+This index is a quick-reference of helper functions in the test suite.
+Use it to determine whether you can reuse a helper, instead of writing new code,
+to improve readability and reviewability.
+
+Whenever a new generally-useful helper is added, it should be indexed here.
+
+**See linked documentation for full helper listings.**
+
+- {@link common/framework/params_builder!CaseParamsBuilder} and {@link common/framework/params_builder!SubcaseParamsBuilder}:
+ Combinatorial generation of test parameters. They are iterated by the test framework at runtime.
+ See `examples.spec.ts` for basic examples of how this behaves.
+ - {@link common/framework/params_builder!CaseParamsBuilder}:
+ `ParamsBuilder` for adding "cases" to a test.
+ - {@link common/framework/params_builder!CaseParamsBuilder#beginSubcases}:
+ "Finalizes" the `CaseParamsBuilder`, returning a `SubcaseParamsBuilder`.
+ - {@link common/framework/params_builder!SubcaseParamsBuilder}:
+ `ParamsBuilder` for adding "subcases" to a test.
+
+### Fixtures
+
+(Uncheck the "Inherited" box to hide inherited methods from documentation pages.)
+
+- {@link common/framework/fixture!Fixture}: Base fixture for all tests.
+- {@link webgpu/gpu_test!GPUTest}: Base fixture for WebGPU tests.
+- {@link webgpu/api/validation/validation_test!ValidationTest}: Base fixture for WebGPU validation tests.
+- {@link webgpu/shader/validation/shader_validation_test!ShaderValidationTest}: Base fixture for WGSL shader validation tests.
+- {@link webgpu/idl/idl_test!IDLTest}:
+ Base fixture for testing the exposed interface is correct (without actually using WebGPU).
+
+### WebGPU Helpers
+
+- {@link webgpu/capability_info}: Structured information about texture formats, binding types, etc.
+- {@link webgpu/constants}:
+ Constant values (needed anytime a WebGPU constant is needed outside of a test function).
+- {@link webgpu/util/buffer}: Helpers for GPUBuffers.
+- {@link webgpu/util/texture}: Helpers for GPUTextures.
+- {@link webgpu/util/unions}: Helpers for various union typedefs in the WebGPU spec.
+- {@link webgpu/util/math}: Helpers for common math operations.
+- {@link webgpu/util/check_contents}: Check the contents of TypedArrays, with nice messages.
+ Also can be composed with {@link webgpu/gpu_test!GPUTest#expectGPUBufferValuesPassCheck}, used to implement
+ GPUBuffer checking helpers in GPUTest.
+- {@link webgpu/util/conversion}: Numeric encoding/decoding for float/unorm/snorm values, etc.
+- {@link webgpu/util/copy_to_texture}:
+ Helper class for copyToTexture test suites for execution copy and check results.
+- {@link webgpu/util/color_space_conversion}:
+ Helper functions to do color space conversion. The algorithm is the same as defined in
+ CSS Color Module Level 4.
+- {@link webgpu/util/create_elements}:
+ Helpers for creating web elements like HTMLCanvasElement, OffscreenCanvas, etc.
+- {@link webgpu/util/shader}: Helpers for creating fragment shader based on intended output values, plainType, and componentCount.
+- {@link webgpu/util/texture/base}: General texture-related helpers.
+- {@link webgpu/util/texture/data_generation}: Helper for generating dummy texture data.
+- {@link webgpu/util/texture/layout}: Helpers for working with linear image data
+ (like in copyBufferToTexture, copyTextureToBuffer, writeTexture).
+- {@link webgpu/util/texture/subresource}: Helpers for working with texture subresource ranges.
+- {@link webgpu/util/texture/texel_data}: Helpers encoding/decoding texel formats.
+- {@link webgpu/util/texture/texel_view}: Helper class to create and view texture data through various representations.
+- {@link webgpu/util/texture/texture_ok}: Helpers for checking texture contents.
+- {@link webgpu/shader/types}: Helpers for WGSL data types.
+- {@link webgpu/shader/execution/expression/expression}: Helpers for WGSL expression execution tests.
+- {@link webgpu/web_platform/util}: Helpers for web platform features (e.g. video elements).
+
+### General Helpers
+
+- {@link common/framework/resources}: Provides the path to the `resources/` directory.
+- {@link common/util/navigator_gpu}: Finds and returns the `navigator.gpu` object or equivalent.
+- {@link common/util/util}: Miscellaneous utilities.
+ - {@link common/util/util!assert}: Assert a condition, otherwise throw an exception.
+ - {@link common/util/util!unreachable}: Assert unreachable code.
+ - {@link common/util/util!assertReject}, {@link common/util/util!resolveOnTimeout},
+ {@link common/util/util!rejectOnTimeout},
+ {@link common/util/util!raceWithRejectOnTimeout}, and more.
+- {@link common/util/collect_garbage}:
+ Attempt to trigger garbage collection, for testing that garbage collection is not observable.
+- {@link common/util/preprocessor}: A simple template-based, non-line-based preprocessor,
+ implementing if/elif/else/endif. Possibly useful for WGSL shader generation.
+- {@link common/util/timeout}: Use this instead of `setTimeout`.
+- {@link common/util/types}: Type metaprogramming helpers.
diff --git a/dom/webgpu/tests/cts/checkout/docs/implementing.md b/dom/webgpu/tests/cts/checkout/docs/implementing.md
new file mode 100644
index 0000000000..ae6848839a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/implementing.md
@@ -0,0 +1,97 @@
+# Test Implementation
+
+Concepts important to understand when writing tests. See existing tests for examples to copy from.
+
+## Test fixtures
+
+Most tests can use one of the several common test fixtures:
+
+- `Fixture`: Base fixture, provides core functions like `expect()`, `skip()`.
+- `GPUTest`: Wraps every test in error scopes. Provides helpers like `expectContents()`.
+- `ValidationTest`: Extends `GPUTest`, provides helpers like `expectValidationError()`, `getErrorTextureView()`.
+- Or create your own. (Often not necessary - helper functions can be used instead.)
+
+Test fixtures or helper functions may be defined in `.spec.ts` files, but if used by multiple
+test files, should be defined in separate `.ts` files (without `.spec`) alongside the files that
+use them.
+
+### GPUDevices in tests
+
+`GPUDevice`s are largely stateless (except for `lost`-ness, error scope stack, and `label`).
+This allows the CTS to reuse one device across multiple test cases using the `DevicePool`,
+which provides `GPUDevice` objects to tests.
+
+Currently, there is one `GPUDevice` with the default descriptor, and
+a cache of several more, for devices with additional capabilities.
+Devices in the `DevicePool` are automatically removed when certain things go wrong.
+
+Later, there may be multiple `GPUDevice`s to allow multiple test cases to run concurrently.
+
+## Test parameterization
+
+The CTS provides helpers (`.params()` and friends) for creating large cartesian products of test parameters.
+These generate "test cases" further subdivided into "test subcases".
+See `basic,*` in `examples.spec.ts` for examples, and the [helper index](./helper_index.txt)
+for a list of capabilities.
+
+Test parameterization should be applied liberally to ensure the maximum coverage
+possible within reasonable time. You can skip some with `.filter()`. And remember: computers are
+pretty fast - thousands of test cases can be reasonable.
+
+Use existing lists of parameters values (such as
+[`kTextureFormats`](https://github.com/gpuweb/cts/blob/0f38b85/src/suites/cts/capability_info.ts#L61),
+to parameterize tests), instead of making your own list. Use the info tables (such as
+`kTextureFormatInfo`) to define and retrieve information about the parameters.
+
+## Asynchrony in tests
+
+Since there are no synchronous operations in WebGPU, almost every test is asynchronous in some
+way. For example:
+
+- Checking the result of a readback.
+- Capturing the result of a `popErrorScope()`.
+
+That said, test functions don't always need to be `async`; see below.
+
+### Checking asynchronous errors/results
+
+Validation is inherently asynchronous (`popErrorScope()` returns a promise). However, the error
+scope stack itself is synchronous - operations immediately after a `popErrorScope()` are outside
+that error scope.
+
+As a result, tests can assert things like validation errors/successes without having an `async`
+test body.
+
+**Example:**
+
+```typescript
+t.expectValidationError(() => {
+ device.createThing();
+});
+```
+
+does:
+
+- `pushErrorScope('validation')`
+- `popErrorScope()` and "eventually" check whether it returned an error.
+
+**Example:**
+
+```typescript
+t.expectGPUBufferValuesEqual(srcBuffer, expectedData);
+```
+
+does:
+
+- copy `srcBuffer` into a new mappable buffer `dst`
+- `dst.mapReadAsync()`, and "eventually" check what data it returned.
+
+Internally, this is accomplished via an "eventual expectation": `eventualAsyncExpectation()`
+takes an async function, calls it immediately, and stores off the resulting `Promise` to
+automatically await at the end before determining the pass/fail state.
+
+### Asynchronous parallelism
+
+A side effect of test asynchrony is that it's possible for multiple tests to be in flight at
+once. We do not currently do this, but it will eventually be an option to run `N` tests in
+"parallel", for faster local test runs.
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/README.md b/dom/webgpu/tests/cts/checkout/docs/intro/README.md
new file mode 100644
index 0000000000..e5f8bcedc6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/README.md
@@ -0,0 +1,99 @@
+# Introduction
+
+These documents contains guidelines for contributors to the WebGPU CTS (Conformance Test Suite)
+on how to write effective tests, and on the testing philosophy to adopt.
+
+The WebGPU CTS is arguably more important than the WebGPU specification itself, because
+it is what forces implementation to be interoperable by checking they conform to the specification.
+However writing a CTS is hard and requires a lot of effort to reach good coverage.
+
+More than a collection of tests like regular end2end and unit tests for software artifacts, a CTS
+needs to be exhaustive. Contrast for example the WebGL2 CTS with the ANGLE end2end tests: they
+cover the same functionality (WebGL 2 / OpenGL ES 3) but are structured very differently:
+
+- ANGLE's test suite has one or two tests per functionality to check it works correctly, plus
+ regression tests and special tests to cover implementation details.
+- WebGL2's CTS can have thousands of tests per API aspect to cover every combination of
+ parameters (and global state) used by an operation.
+
+Below are guidelines based on our collective experience with graphics API CTSes like WebGL's.
+They are expected to evolve over time and have exceptions, but should give a general idea of what
+to do.
+
+## Contributing
+
+Testing tasks are tracked in the [CTS project tracker](https://github.com/orgs/gpuweb/projects/3).
+Go here if you're looking for tasks, or if you have a test idea that isn't already covered.
+
+If contributing conformance tests, the directory you'll work in is [`src/webgpu/`](../src/webgpu/).
+This directory is organized according to the goal of the test (API validation behavior vs
+actual results) and its target (API entry points and spec areas, e.g. texture sampling).
+
+The contents of a test file (`src/webgpu/**/*.spec.ts`) are twofold:
+
+- Documentation ("test plans") on what tests do, how they do it, and what cases they cover.
+ Some test plans are fully or partially unimplemented:
+ they either contain "TODO" in a description or are `.unimplemented()`.
+- Actual tests.
+
+**Please read the following short documents before contributing.**
+
+### 0. [Developing](developing.md)
+
+- Reviewers should also read [Review Requirements](../reviews.md).
+
+### 1. [Life of a Test Change](life_of.md)
+
+### 2. [Adding or Editing Test Plans](plans.md)
+
+### 3. [Implementing Tests](tests.md)
+
+## [Additional Documentation](../)
+
+## Examples
+
+### Operation testing of vertex input id generation
+
+This section provides an example of the planning process for a test.
+It has not been refined into a set of final test plan descriptions.
+(Note: this predates the actual implementation of these tests, so doesn't match the actual tests.)
+
+Somewhere under the `api/operation` node are tests checking that running `GPURenderPipelines` on
+the device using the `GPURenderEncoderBase.draw` family of functions works correctly. Render
+pipelines are composed of several stages that are mostly independent so they can be split in
+several parts such as `vertex_input`, `rasterization`, `blending`.
+
+Vertex input itself has several parts that are mostly separate in hardware:
+
+- generation of the vertex and instance indices to run for this draw
+- fetching of vertex data from vertex buffers based on these indices
+- conversion from the vertex attribute `GPUVertexFormat` to the datatype for the input variable
+ in the shader
+
+Each of these are tested separately and have cases for each combination of the variables that may
+affect them. This means that `api/operation/render/vertex_input/id_generation` checks that the
+correct operation is performed for the cartesian product of all the following dimensions:
+
+- for encoding in a `GPURenderPassEncoder` or a `GPURenderBundleEncoder`
+- whether the draw is direct or indirect
+- whether the draw is indexed or not
+- for various values of the `firstInstance` argument
+- for various values of the `instanceCount` argument
+- if the draw is not indexed:
+ - for various values of the `firstVertex` argument
+ - for various values of the `vertexCount` argument
+- if the draw is indexed:
+ - for each `GPUIndexFormat`
+ - for various values of the indices in the index buffer including the primitive restart values
+ - for various values for the `offset` argument to `setIndexBuffer`
+ - for various values of the `firstIndex` argument
+ - for various values of the `indexCount` argument
+ - for various values of the `baseVertex` argument
+
+"Various values" above mean several small values, including `0` and the second smallest valid
+value to check for corner cases, as well as some large value.
+
+An instance of the test sets up a `draw*` call based on the parameters, using point rendering and
+a fragment shader that outputs to a storage buffer. After the draw the test checks the content of
+the storage buffer to make sure all expected vertex shader invocation, and only these ones have
+been generated.
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/convert_to_issue.png b/dom/webgpu/tests/cts/checkout/docs/intro/convert_to_issue.png
new file mode 100644
index 0000000000..672324a9d9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/convert_to_issue.png
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/developing.md b/dom/webgpu/tests/cts/checkout/docs/intro/developing.md
new file mode 100644
index 0000000000..5b1aeed36d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/developing.md
@@ -0,0 +1,134 @@
+# Developing
+
+The WebGPU CTS is written in TypeScript.
+
+## Setup
+
+After checking out the repository and installing node/npm, run:
+
+```sh
+npm ci
+```
+
+Before uploading, you can run pre-submit checks (`npm test`) to make sure it will pass CI.
+Use `npm run fix` to fix linting issues.
+
+`npm run` will show available npm scripts.
+Some more scripts can be listed using `npx grunt`.
+
+## Dev Server
+
+To start the development server, use:
+
+```sh
+npm start
+```
+
+Then, browse to the standalone test runner at the printed URL.
+
+The server will generate and compile code on the fly, so no build step is necessary.
+Only a reload is needed to see saved changes.
+(TODO: except, currently, `README.txt` and file `description` changes won't be reflected in
+the standalone runner.)
+
+Note: The first load of a test suite may take some time as generating the test suite listing can
+take a few seconds.
+
+## Standalone Test Runner / Test Plan Viewer
+
+**The standalone test runner also serves as a test plan viewer.**
+(This can be done in a browser without WebGPU support.)
+You can use this to preview how your test plan will appear.
+
+You can view different suites (webgpu, unittests, stress, etc.) or different subtrees of
+the test suite.
+
+- `http://localhost:8080/standalone/` (defaults to `?runnow=0&worker=0&debug=0&q=webgpu:*`)
+- `http://localhost:8080/standalone/?q=unittests:*`
+- `http://localhost:8080/standalone/?q=unittests:basic:*`
+
+The following url parameters change how the harness runs:
+
+- `runnow=1` runs all matching tests on page load.
+- `debug=1` enables verbose debug logging from tests.
+- `worker=1` runs the tests on a Web Worker instead of the main thread.
+- `power_preference=low-power` runs most tests passing `powerPreference: low-power` to `requestAdapter`
+- `power_preference=high-performance` runs most tests passing `powerPreference: high-performance` to `requestAdapter`
+
+### Web Platform Tests (wpt) - Ref Tests
+
+You can inspect the actual and reference pages for web platform reftests in the standalone
+runner by navigating to them. For example, by loading:
+
+ - `http://localhost:8080/out/webgpu/web_platform/reftests/canvas_clear.https.html`
+ - `http://localhost:8080/out/webgpu/web_platform/reftests/ref/canvas_clear-ref.html`
+
+You can also run a minimal ref test runner.
+
+ - open 2 terminals / command lines.
+ - in one, `npm start`
+ - in the other, `node tools/run_wpt_ref_tests <path-to-browser-executable> [name-of-test]`
+
+Without `[name-of-test]` all ref tests will be run. `[name-of-test]` is just a simple check for
+substring so passing in `rgba` will run every test with `rgba` in its filename.
+
+Examples:
+
+MacOS
+
+```
+# Chrome
+node tools/run_wpt_ref_tests /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary
+```
+
+Windows
+
+```
+# Chrome
+node .\tools\run_wpt_ref_tests "C:\Users\your-user-name\AppData\Local\Google\Chrome SxS\Application\chrome.exe"
+```
+
+## Editor
+
+Since this project is written in TypeScript, it integrates best with
+[Visual Studio Code](https://code.visualstudio.com/).
+This is optional, but highly recommended: it automatically adds `import` lines and
+provides robust completions, cross-references, renames, error highlighting,
+deprecation highlighting, and type/JSDoc popups.
+
+Open the `cts.code-workspace` workspace file to load settings convenient for this project.
+You can make local configuration changes in `.vscode/`, which is untracked by Git.
+
+## Pull Requests
+
+When opening a pull request, fill out the PR checklist and attach the issue number.
+If an issue hasn't been opened, find the draft issue on the
+[project tracker](https://github.com/orgs/gpuweb/projects/3) and choose "Convert to issue":
+
+![convert to issue button screenshot](convert_to_issue.png)
+
+Opening a pull request will automatically notify reviewers.
+
+To make the review process smoother, once a reviewer has started looking at your change:
+
+- Avoid major additions or changes that would be best done in a follow-up PR.
+- Avoid rebases (`git rebase`) and force pushes (`git push -f`). These can make
+ it difficult for reviewers to review incremental changes as GitHub often cannot
+ view a useful diff across a rebase. If it's necessary to resolve conflicts
+ with upstream changes, use a merge commit (`git merge`) and don't include any
+ consequential changes in the merge, so a reviewer can skip over merge commits
+ when working through the individual commits in the PR.
+- When you address a review comment, mark the thread as "Resolved".
+
+Pull requests will (usually) be landed with the "Squash and merge" option.
+
+### TODOs
+
+The word "TODO" refers to missing test coverage. It may only appear inside file/test descriptions
+and README files (enforced by linting).
+
+To use comments to refer to TODOs inside the description, use a backreference, e.g., in the
+description, `TODO: Also test the FROBNICATE usage flag [1]`, and somewhere in the code, `[1]:
+Need to add FROBNICATE to this list.`.
+
+Use `MAINTENANCE_TODO` for TODOs which don't impact test coverage.
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/life_of.md b/dom/webgpu/tests/cts/checkout/docs/intro/life_of.md
new file mode 100644
index 0000000000..8dced4ad84
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/life_of.md
@@ -0,0 +1,46 @@
+# Life of a Test Change
+
+A "test change" could be a new test, an expansion of an existing test, a test bug fix, or a
+modification to existing tests to make them match new spec changes.
+
+**CTS contributors should contribute to the tracker and strive to keep it up to date, especially
+relating to their own changes.**
+
+Filing new draft issues in the CTS project tracker is very lightweight.
+Anyone with access should do this eagerly, to ensure no testing ideas are forgotten.
+(And if you don't have access, just file a regular issue.)
+
+1. Enter a [draft issue](https://github.com/orgs/gpuweb/projects/3), with the Status
+ set to "New (not in repo)", and any available info included in the issue description
+ (notes/plans to ensure full test coverage of the change). The source of this may be:
+
+ - Anything in the spec/API that is found not to be covered by the CTS yet.
+ - Any test is found to be outdated or otherwise buggy.
+ - A spec change from the "Needs CTS Issue" column in the
+ [spec project tracker](https://github.com/orgs/gpuweb/projects/1).
+ Once information on the required test changes is entered into the CTS project tracker,
+ the spec issue moves to "Specification Done".
+
+ Note: at some point, someone may make a PR to flush "New (not in repo)" issues into `TODO`s in
+ CTS file/test description text, changing their "Status" to "Open".
+ These may be done in bulk without linking back to the issue.
+
+1. As necessary:
+
+ - Convert the draft issue to a full, numbered issue for linking from later PRs.
+
+ ![convert to issue button screenshot](convert_to_issue.png)
+
+ - Update the "Assignees" of the issue when an issue is assigned or unassigned
+ (you can assign yourself).
+ - Change the "Status" of the issue to "Started" once you start the task.
+
+1. Open one or more PRs, **each linking to the associated issue**.
+ Each PR may is reviewed and landed, and may leave further TODOs for parts it doesn't complete.
+
+ 1. Test are "planned" in test descriptions. (For complex tests, open a separate PR with the
+ tests `.unimplemented()` so a reviewer can evaluate the plan before you implement tests.)
+ 1. Tests are implemented.
+
+1. When **no TODOs remain** for an issue, close it and change its status to "Complete".
+ (Enter a new more, specific draft issue into the tracker if you need to track related TODOs.)
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/plans.md b/dom/webgpu/tests/cts/checkout/docs/intro/plans.md
new file mode 100644
index 0000000000..f8d7af3a78
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/plans.md
@@ -0,0 +1,82 @@
+# Adding or Editing Test Plans
+
+## 1. Write a test plan
+
+For new tests, if some notes exist already, incorporate them into your plan.
+
+A detailed test plan should be written and reviewed before substantial test code is written.
+This allows reviewers a chance to identify additional tests and cases, opportunities for
+generalizations that would improve the strength of tests, similar existing tests or test plans,
+and potentially useful [helpers](../helper_index.txt).
+
+**A test plan must serve two functions:**
+
+- Describes the test, succinctly, but in enough detail that a reader can read *only* the test
+ plans and evaluate coverage completeness of a file/directory.
+- Describes the test precisely enough that, when code is added, the reviewer can ensure that the
+ test really covers what the test plan says.
+
+There should be one test plan for each test. It should describe what it tests, how, and describe
+important cases that need to be covered. Here's an example:
+
+```ts
+g.test('x,some_detail')
+ .desc(
+ `
+Tests [some detail] about x. Tests calling x in various 'mode's { mode1, mode2 },
+with various values of 'arg', and checks correctness of the result.
+Tries to trigger [some conditional path].
+
+- Valid values (control case) // <- (to make sure the test function works well)
+- Unaligned values (should fail) // <- (only validation tests need to intentionally hit invalid cases)
+- Extreme values`
+ )
+ .params(u =>
+ u //
+ .combine('mode', ['mode1', 'mode2'])
+ .beginSubcases()
+ .combine('arg', [
+ // Valid // <- Comment params as you see fit.
+ 4,
+ 8,
+ 100,
+ // Invalid
+ 2,
+ 6,
+ 1e30,
+ ])
+ )
+ .unimplemented();
+```
+
+"Cases" each appear as individual items in the `/standalone/` runner.
+"Subcases" run inside each case, like a for-loop wrapping the `.fn(`test function`)`.
+Documentation on the parameter builder can be found in the [helper index](../helper_index.txt).
+
+It's often impossible to predict the exact case/subcase structure before implementing tests, so they
+can be added during implementation, instead of planning.
+
+For any notes which are not specific to a single test, or for preliminary notes for tests that
+haven't been planned in full detail, put them in the test file's `description` variable at
+the top. Or, if they aren't associated with a test file, put them in a `README.txt` file.
+
+**Any notes about missing test coverage must be marked with the word `TODO` inside a
+description or README.** This makes them appear on the `/standalone/` page.
+
+## 2. Open a pull request
+
+Open a PR, and work with the reviewer(s) to revise the test plan.
+
+Usually (probably), plans will be landed in separate PRs before test implementations.
+
+## Conventions used in test plans
+
+- `Iff`: If and only if
+- `x=`: "cartesian-cross equals", like `+=` for cartesian product.
+ Used for combinatorial test coverage.
+ - Sometimes this will result in too many test cases; simplify/reduce as needed
+ during planning *or* implementation.
+- `{x,y,z}`: list of cases to test
+ - e.g. `x= texture format {r8unorm, r8snorm}`
+- *Control case*: a case included to make sure that the rest of the cases aren't
+ missing their target by testing some other error case.
diff --git a/dom/webgpu/tests/cts/checkout/docs/intro/tests.md b/dom/webgpu/tests/cts/checkout/docs/intro/tests.md
new file mode 100644
index 0000000000..a67b6a20cc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/intro/tests.md
@@ -0,0 +1,25 @@
+# Implementing Tests
+
+Once a test plan is done, you can start writing tests.
+To add new tests, imitate the pattern in neigboring tests or neighboring files.
+New test files must be named ending in `.spec.ts`.
+
+For an example test file, see [`src/webgpu/examples.spec.ts`](../../src/webgpu/examples.spec.ts).
+For a more complex, well-structured reference test file, see
+[`src/webgpu/api/validation/vertex_state.spec.ts`](../../src/webgpu/api/validation/vertex_state.spec.ts).
+
+Implement some tests and open a pull request. You can open a PR any time you're ready for a review.
+(If two tests are non-trivial but independent, consider separate pull requests.)
+
+Before uploading, you can run pre-submit checks (`npm test`) to make sure it will pass CI.
+Use `npm run fix` to fix linting issues.
+
+## Test Helpers
+
+It's best to be familiar with helpers available in the test suite for simplifying
+test implementations.
+
+New test helpers can be added at any time to either of those files, or to new `.ts` files anywhere
+near the `.spec.ts` file where they're used.
+
+Documentation on existing helpers can be found in the [helper index](../helper_index.txt).
diff --git a/dom/webgpu/tests/cts/checkout/docs/organization.md b/dom/webgpu/tests/cts/checkout/docs/organization.md
new file mode 100644
index 0000000000..fd7020afd6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/organization.md
@@ -0,0 +1,166 @@
+# Test Organization
+
+## `src/webgpu/`
+
+Because of the glorious amount of test needed, the WebGPU CTS is organized as a tree of arbitrary
+depth (a filesystem with multiple tests per file).
+
+Each directory may have a `README.txt` describing its contents.
+Tests are grouped in large families (each of which has a `README.txt`);
+the root and first few levels looks like the following (some nodes omitted for simplicity):
+
+- **`api`** with tests for full coverage of the Javascript API surface of WebGPU.
+ - **`validation`** with positive and negative tests for all the validation rules of the API.
+ - **`operation`** with tests that checks the result of performing valid WebGPU operations,
+ taking advantage of parametrization to exercise interactions between parts of the API.
+ - **`regression`** for one-off tests that reproduce bugs found in implementations to prevent
+ the bugs from appearing again.
+- **`shader`** with tests for full coverage of the shaders that can be passed to WebGPU.
+ - **`validation`**.
+ - **`execution`** similar to `api/operation`.
+ - **`regression`**.
+- **`idl`** with tests to check that the WebGPU IDL is correctly implemented, for examples that
+ objects exposed exactly the correct members, and that methods throw when passed incomplete
+ dictionaries.
+- **`web-platform`** with tests for Web platform-specific interactions like `GPUSwapChain` and
+ `<canvas>`, WebXR and `GPUQueue.copyExternalImageToTexture`.
+
+At the same time test hierarchies can be used to split the testing of a single sub-object into
+several file for maintainability. For example `GPURenderPipeline` has a large descriptor and some
+parts could be tested independently like `vertex_input` vs. `primitive_topology` vs. `blending`
+but all live under the `render_pipeline` directory.
+
+In addition to the test tree, each test can be parameterized. For coverage it is important to
+test all enums values, for example for `GPUTextureFormat`. Instead of having a loop to iterate
+over all the `GPUTextureFormat`, it is better to parameterize the test over them. Each format
+will have a different entry in the test list which will help WebGPU implementers debug the test,
+or suppress the failure without losing test coverage while they fix the bug.
+
+Extra capabilities (limits and features) are often tested in the same files as the rest of the API.
+For example, a compressed texture format capability would simply add a `GPUTextureFormat` to the
+parametrization lists of many tests, while a capability adding significant new functionality
+like ray-tracing could have a separate subtree.
+
+Operation tests for optional features should be skipped using `t.selectDeviceOrSkipTestCase()` or
+`t.skip()`. Validation tests should be written that test the behavior with and without the
+capability enabled via `t.selectDeviceOrSkipTestCase()`, to ensure the functionality is valid
+only with the capability enabled.
+
+### Validation tests
+
+Validation tests check the validation rules that are (or will be) set by the
+WebGPU spec. Validation tests try to carefully trigger the individual validation
+rules in the spec, without simultaneously triggering other rules.
+
+Validation errors *generally* generate WebGPU errors, not exceptions.
+But check the spec on a case-by-case basis.
+
+Like all `GPUTest`s, `ValidationTest`s are wrapped in both types of error scope. These
+"catch-all" error scopes look for any errors during the test, and report them as test failures.
+Since error scopes can be nested, validation tests can nest an error scope to expect that there
+*are* errors from specific operations.
+
+#### Parameterization
+
+Test parameterization can help write many validation tests more succinctly,
+while making it easier for both authors and reviewers to be confident that
+an aspect of the API is tested fully. Examples:
+
+- [`webgpu:api,validation,render_pass,resolve:resolve_attachment:*`](https://github.com/gpuweb/cts/blob/ded3b7c8a4680a1a01621a8ac859facefadf32d0/src/webgpu/api/validation/render_pass/resolve.spec.ts#L35)
+- [`webgpu:api,validation,createBindGroupLayout:bindingTypeSpecific_optional_members:*`](https://github.com/gpuweb/cts/blob/ded3b7c8a4680a1a01621a8ac859facefadf32d0/src/webgpu/api/validation/createBindGroupLayout.spec.ts#L68)
+
+Use your own discretion when deciding the balance between heavily parameterizing
+a test and writing multiple separate tests.
+
+#### Guidelines
+
+There are many aspects that should be tested in all validation tests:
+
+- each individual argument to a method call (including `this`) or member of a descriptor
+ dictionary should be tested including:
+ - what happens when an error object is passed.
+ - what happens when an optional feature enum or method is used.
+ - what happens for numeric values when they are at 0, too large, too small, etc.
+- each validation rule in the specification should be checked both with a control success case,
+ and error cases.
+- each set of arguments or state that interact for validation.
+
+When testing numeric values, it is important to check on both sides of the boundary: if the error
+happens for value N and not N - 1, both should be tested. Alignment of integer values should also
+be tested but boundary testing of alignment should be between a value aligned to 2^N and a value
+aligned to 2^(N-1).
+
+Finally, this is probably also where we would test that extensions follow the rule that: if the
+browser supports a feature but it is not enabled on the device, then calling methods from that
+feature throws `TypeError`.
+
+- Test providing unknown properties *that are definitely not part of any feature* are
+ valid/ignored. (Unfortunately, due to the rules of IDL, adding a member to a dictionary is
+ always a breaking change. So this is how we have to test this unless we can get a "strict"
+ dictionary type in IDL. We can't test adding members from non-enabled extensions.)
+
+### Operation tests
+
+Operation tests test the actual results of using the API. They execute
+(sometimes significant) code and check that the result is within the expected
+set of behaviors (which can be quite complex to compute).
+
+Note that operation tests need to test a lot of interactions between different
+parts of the API, and so can become quite complex. Try to reduce the complexity by
+utilizing combinatorics and [helpers](./helper_index.txt), and splitting/merging test files as needed.
+
+#### Errors
+
+Operation tests are usually `GPUTest`s. As a result, they automatically fail on any validation
+errors that occur during the test.
+
+When it's easier to write an operation test with invalid cases, use
+`ParamsBuilder.filter`/`.unless` to avoid invalid cases, or detect and
+`expect` validation errors in some cases.
+
+#### Implementation
+
+Use helpers like `expectContents` (and more to come) to check the values of data on the GPU.
+(These are "eventual expectations" - the harness will wait for them to finish at the end).
+
+When testing something inside a shader, it's not always necessary to output the result to a
+render output. In fragment shaders, you can output to a storage buffer. In vertex shaders, you
+can't - but you can render with points (simplest), send the result to the fragment shader, and
+output it from there. (Someday, we may end up wanting a helper for this.)
+
+#### Testing Default Values
+
+Default value tests (for arguments and dictionary members) should usually be operation tests -
+all you have to do is include `undefined` in parameterizations of other tests to make sure the
+behavior with `undefined` has the same expected result that you have when the default value is
+specified explicitly.
+
+### IDL tests
+
+TODO: figure out how to implement these. https://github.com/gpuweb/cts/issues/332
+
+These tests test only rules that come directly from WebIDL. For example:
+
+- Values out of range for `[EnforceRange]` cause exceptions.
+- Required function arguments and dictionary members cause exceptions if omitted.
+- Arguments and dictionary members cause exceptions if passed the wrong type.
+
+They may also test positive cases like the following, but the behavior of these should be tested in
+operation tests.
+
+- OK to omit optional arguments/members.
+- OK to pass the correct argument/member type (or of any type in a union type).
+
+Every overload of every method should be tested.
+
+## `src/stress/`, `src/manual/`
+
+Stress tests and manual tests for WebGPU that are not intended to be run in an automated way.
+
+## `src/unittests/`
+
+Unit tests for the test framework (`src/common/framework/`).
+
+## `src/demo/`
+
+A demo of test hierarchies for the purpose of testing the `standalone` test runner page.
diff --git a/dom/webgpu/tests/cts/checkout/docs/reviews.md b/dom/webgpu/tests/cts/checkout/docs/reviews.md
new file mode 100644
index 0000000000..1a8c3f9624
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/reviews.md
@@ -0,0 +1,70 @@
+# Review Requirements
+
+A review should have several items checked off before it is landed.
+Checkboxes are pre-filled into the pull request summary when it's created.
+
+The uploader may pre-check-off boxes if they are not applicable
+(e.g. TypeScript readability on a plan PR).
+
+## Readability
+
+A reviewer has "readability" for a topic if they have enough expertise in that topic to ensure
+good practices are followed in pull requests, or know when to loop in other reviewers.
+Perfection is not required!
+
+**It is up to reviewers' own discretion** whether they are qualified to check off a
+"readability" checkbox on any given pull request.
+
+- WebGPU Readability: Familiarity with the API to ensure:
+
+ - WebGPU is being used correctly; expected results seem reasonable.
+ - WebGPU is being tested completely; tests have control cases.
+ - Test code has a clear correspondence with the test description.
+ - [Test helpers](./helper_index.txt) are used or created appropriately
+ (where the reviewer is familiar with the helpers).
+
+- TypeScript Readability: Make sure TypeScript is utilized in a way that:
+
+ - Ensures test code is reasonably type-safe.
+ Reviewers may recommend changes to make type-safety either weaker (`as`, etc.) or stronger.
+ - Is understandable and has appropriate verbosity and dynamicity
+ (e.g. type inference and `as const` are used to reduce unnecessary boilerplate).
+
+## Plan Reviews
+
+**Changes *must* have an author or reviewer with the following readability:** WebGPU
+
+Reviewers must carefully ensure the following:
+
+- The test plan name accurately describes the area being tested.
+- The test plan covers the area described by the file/test name and file/test description
+ as fully as possible (or adds TODOs for incomplete areas).
+- Validation tests have control cases (where no validation error should occur).
+- Each validation rule is tested in isolation, in at least one case which does not validate any
+ other validation rules.
+
+See also: [Adding or Editing Test Plans](intro/plans.md).
+
+## Implementation Reviews
+
+**Changes *must* have an author or reviewer with the following readability:** WebGPU, TypeScript
+
+Reviewers must carefully ensure the following:
+
+- The coverage of the test implementation precisely matches the test description.
+- Everything required for test plan reviews above.
+
+Reviewers should ensure the following:
+
+- New test helpers are documented in [helper index](./helper_index.txt).
+- Framework and test helpers are used where they would make test code clearer.
+
+See also: [Implementing Tests](intro/tests.md).
+
+## Framework
+
+**Changes *must* have an author or reviewer with the following readability:** TypeScript
+
+Reviewers should ensure the following:
+
+- Changes are reasonably type-safe, and covered by unit tests where appropriate.
diff --git a/dom/webgpu/tests/cts/checkout/docs/terms.md b/dom/webgpu/tests/cts/checkout/docs/terms.md
new file mode 100644
index 0000000000..032639be57
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/docs/terms.md
@@ -0,0 +1,270 @@
+# Terminology
+
+Each test suite is organized as a tree, both in the filesystem and further within each file.
+
+- _Suites_, e.g. `src/webgpu/`.
+ - _READMEs_, e.g. `src/webgpu/README.txt`.
+ - _Test Spec Files_, e.g. `src/webgpu/examples.spec.ts`.
+ Identified by their file path.
+ Each test spec file provides a description and a _Test Group_.
+ A _Test Group_ defines a test fixture, and contains multiple:
+ - _Tests_.
+ Identified by a comma-separated list of parts (e.g. `basic,async`)
+ which define a path through a filesystem-like tree (analogy: `basic/async.txt`).
+ Defines a _test function_ and contains multiple:
+ - _Test Cases_.
+ Identified by a list of _Public Parameters_ (e.g. `x` = `1`, `y` = `2`).
+ Each Test Case has the same test function but different Public Parameters.
+
+## Test Tree
+
+A _Test Tree_ is a tree whose leaves are individual Test Cases.
+
+A Test Tree can be thought of as follows:
+
+- Suite, which is the root of a tree with "leaves" which are:
+ - Test Spec Files, each of which is a tree with "leaves" which are:
+ - Tests, each of which is a tree with leaves which are:
+ - Test Cases.
+
+(In the implementation, this conceptual tree of trees is decomposed into one big tree
+whose leaves are Test Cases.)
+
+**Type:** `TestTree`
+
+## Suite
+
+A suite of tests.
+A single suite has a directory structure, and many _test spec files_
+(`.spec.ts` files containing tests) and _READMEs_.
+Each member of a suite is identified by its path within the suite.
+
+**Example:** `src/webgpu/`
+
+### README
+
+**Example:** `src/webgpu/README.txt`
+
+Describes (in prose) the contents of a subdirectory in a suite.
+
+READMEs are only processed at build time, when generating the _Listing_ for a suite.
+
+**Type:** `TestSuiteListingEntryReadme`
+
+## Queries
+
+A _Query_ is a structured object which specifies a subset of cases in exactly one Suite.
+A Query can be represented uniquely as a string.
+Queries are used to:
+
+- Identify a subtree of a suite (by identifying the root node of that subtree).
+- Identify individual cases.
+- Represent the list of tests that a test runner (standalone, wpt, or cmdline) should run.
+- Identify subtrees which should not be "collapsed" during WPT `cts.https.html` generation,
+ so that that cts.https.html "variants" can have individual test expectations
+ (i.e. marked as "expected to fail", "skip", etc.).
+
+There are four types of `TestQuery`:
+
+- `TestQueryMultiFile` represents any subtree of the file hierarchy:
+ - `suite:*`
+ - `suite:path,to,*`
+ - `suite:path,to,file,*`
+- `TestQueryMultiTest` represents any subtree of the test hierarchy:
+ - `suite:path,to,file:*`
+ - `suite:path,to,file:path,to,*`
+ - `suite:path,to,file:path,to,test,*`
+- `TestQueryMultiCase` represents any subtree of the case hierarchy:
+ - `suite:path,to,file:path,to,test:*`
+ - `suite:path,to,file:path,to,test:my=0;*`
+ - `suite:path,to,file:path,to,test:my=0;params="here";*`
+- `TestQuerySingleCase` represents as single case:
+ - `suite:path,to,file:path,to,test:my=0;params="here"`
+
+Test Queries are a **weakly ordered set**: any query is
+_Unordered_, _Equal_, _StrictSuperset_, or _StrictSubset_ relative to any other.
+This property is used to construct the complete tree of test cases.
+In the examples above, every example query is a StrictSubset of the previous one
+(note: even `:*` is a subset of `,*`).
+
+In the WPT and standalone harnesses, the query is stored in the URL, e.g.
+`index.html?q=q:u,e:r,y:*`.
+
+Queries are selectively URL-encoded for readability and compatibility with browsers
+(see `encodeURIComponentSelectively`).
+
+**Type:** `TestQuery`
+
+## Listing
+
+A listing of the **test spec files** in a suite.
+
+This can be generated only in Node, which has filesystem access (see `src/tools/crawl.ts`).
+As part of the build step, a _listing file_ is generated (see `src/tools/gen.ts`) so that the
+Test Spec Files can be discovered by the web runner (since it does not have filesystem access).
+
+**Type:** `TestSuiteListing`
+
+### Listing File
+
+Each Suite has one Listing File (`suite/listing.[tj]s`), containing a list of the files
+in the suite.
+
+In `src/suite/listing.ts`, this is computed dynamically.
+In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings`).
+
+**Type:** Once `import`ed, `ListingFile`
+
+**Example:** `out/webgpu/listing.js`
+
+## Test Spec File
+
+A Test Spec File has a `description` and a Test Group (under which tests and cases are defined).
+
+**Type:** Once `import`ed, `SpecFile`
+
+**Example:** `src/webgpu/**/*.spec.ts`
+
+## Test Group
+
+A subtree of tests. There is one Test Group per Test Spec File.
+
+The Test Fixture used for tests is defined at TestGroup creation.
+
+**Type:** `TestGroup`
+
+## Test
+
+One test. It has a single _test function_.
+
+It may represent multiple _test cases_, each of which runs the same Test Function with different
+Parameters.
+
+A test is named using `TestGroup.test()`, which returns a `TestBuilder`.
+`TestBuilder.params()`/`.paramsSimple()`/`.paramsSubcasesOnly()`
+can optionally be used to parametrically generate instances (cases and subcases) of the test.
+Finally, `TestBuilder.fn()` provides the Test Function
+(or, a test can be marked unimplemented with `TestBuilder.unimplemented()`).
+
+### Test Function
+
+When a test subcase is run, the Test Function receives an instance of the
+Test Fixture provided to the Test Group, producing test results.
+
+**Type:** `TestFn`
+
+## Test Case / Case
+
+A single case of a test. It is identified by a `TestCaseID`: a test name, and its parameters.
+
+Each case appears as an individual item (tree leaf) in `/standalone/`,
+and as an individual "step" in WPT.
+
+If `TestBuilder.params()`/`.paramsSimple()`/`.paramsSubcasesOnly()` are not used,
+there is exactly one case with one subcase, with parameters `{}`.
+
+**Type:** During test run time, a case is encapsulated as a `RunCase`.
+
+## Test Subcase / Subcase
+
+A single "subcase" of a test. It can also be identified by a `TestCaseID`, though
+not all contexts allow subdividing cases into subcases.
+
+All of the subcases of a case will run _inside_ the case, essentially as a for-loop wrapping the
+test function. They do _not_ appear individually in `/standalone/` or WPT.
+
+If `CaseParamsBuilder.beginSubcases()` is not used, there is exactly one subcase per case.
+
+## Test Parameters / Params
+
+Each Test Subcase has a (possibly empty) set of Test Parameters,
+The parameters are passed to the Test Function `f(t)` via `t.params`.
+
+A set of Public Parameters identifies a Test Case or Test Subcase within a Test.
+
+There are also Private Parameters: any parameter name beginning with an underscore (`_`).
+These parameters are not part of the Test Case identification, but are still passed into
+the Test Function. They can be used, e.g., to manually specify expected results.
+
+**Type:** `TestParams`
+
+## Test Fixture / Fixture
+
+_Test Fixtures_ provide helpers for tests to use.
+A new instance of the fixture is created for every run of every test case.
+
+There is always one fixture class for a whole test group (though this may change).
+
+The fixture is also how a test gets access to the _case recorder_,
+which allows it to produce test results.
+
+They are also how tests produce results: `.skip()`, `.fail()`, etc.
+
+**Type:** `Fixture`
+
+### `UnitTest` Fixture
+
+Provides basic fixture utilities most useful in the `unittests` suite.
+
+### `GPUTest` Fixture
+
+Provides utilities useful in WebGPU CTS tests.
+
+# Test Results
+
+## Logger
+
+A logger logs the results of a whole test run.
+
+It saves an empty `LiveTestSpecResult` into its results map, then creates a
+_test spec recorder_, which records the results for a group into the `LiveTestSpecResult`.
+
+**Type:** `Logger`
+
+### Test Case Recorder
+
+Refers to a `LiveTestCaseResult` created by the logger.
+Records the results of running a test case (its pass-status, run time, and logs) into it.
+
+**Types:** `TestCaseRecorder`, `LiveTestCaseResult`
+
+#### Test Case Status
+
+The `status` of a `LiveTestCaseResult` can be one of:
+
+- `'running'` (only while still running)
+- `'pass'`
+- `'skip'`
+- `'warn'`
+- `'fail'`
+
+The "worst" result from running a case is always reported (fail > warn > skip > pass).
+Note this means a test can still fail if it's "skipped", if it failed before
+`.skip()` was called.
+
+**Type:** `Status`
+
+## Results Format
+
+The results are returned in JSON format.
+
+They are designed to be easily merged in JavaScript:
+the `"results"` can be passed into the constructor of `Map` and merged from there.
+
+(TODO: Write a merge tool, if needed.)
+
+```js
+{
+ "version": "bf472c5698138cdf801006cd400f587e9b1910a5-dirty",
+ "results": [
+ [
+ "unittests:async_mutex:basic:",
+ { "status": "pass", "timems": 0.286, "logs": [] }
+ ],
+ [
+ "unittests:async_mutex:serial:",
+ { "status": "pass", "timems": 0.415, "logs": [] }
+ ]
+ ]
+}
+```
diff --git a/dom/webgpu/tests/cts/checkout/node.tsconfig.json b/dom/webgpu/tests/cts/checkout/node.tsconfig.json
new file mode 100644
index 0000000000..74707d408d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/node.tsconfig.json
@@ -0,0 +1,20 @@
+// Typescript configuration for compile sources and
+// dependent files for usage directly with Node.js. This
+// is useful for running scripts in tools/ directly with Node
+// without including extra dependencies.
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "incremental": false,
+ "noEmit": false,
+ "declaration": false,
+ },
+
+ "exclude": [
+ "src/common/runtime/wpt.ts",
+ "src/common/runtime/standalone.ts",
+ "src/common/runtime/helper/test_worker.ts",
+ "src/webgpu/web_platform/worker/worker_launcher.ts"
+ ]
+}
diff --git a/dom/webgpu/tests/cts/checkout/package-lock.json b/dom/webgpu/tests/cts/checkout/package-lock.json
new file mode 100644
index 0000000000..325343dbce
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/package-lock.json
@@ -0,0 +1,15798 @@
+{
+ "name": "@webgpu/cts",
+ "version": "0.1.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@webgpu/cts",
+ "version": "0.1.0",
+ "license": "BSD-3-Clause",
+ "devDependencies": {
+ "@babel/cli": "^7.19.3",
+ "@babel/core": "^7.20.5",
+ "@babel/preset-typescript": "^7.18.6",
+ "@types/babel__core": "^7.1.20",
+ "@types/dom-mediacapture-transform": "^0.1.4",
+ "@types/dom-webcodecs": "^0.1.5",
+ "@types/express": "^4.17.14",
+ "@types/jquery": "^3.5.14",
+ "@types/morgan": "^1.9.3",
+ "@types/node": "^14.18.12",
+ "@types/offscreencanvas": "^2019.7.0",
+ "@types/pngjs": "^6.0.1",
+ "@types/serve-index": "^1.9.1",
+ "@typescript-eslint/parser": "^4.33.0",
+ "@webgpu/types": "0.1.25",
+ "ansi-colors": "4.1.1",
+ "babel-plugin-add-header-comment": "^1.0.3",
+ "babel-plugin-const-enum": "^1.2.0",
+ "chokidar": "^3.5.3",
+ "eslint": "^7.11.0",
+ "eslint-plugin-ban": "^1.6.0",
+ "eslint-plugin-deprecation": "^1.3.3",
+ "eslint-plugin-import": "^2.26.0",
+ "express": "^4.18.2",
+ "grunt": "^1.5.3",
+ "grunt-cli": "^1.4.3",
+ "grunt-contrib-clean": "^2.0.1",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-run": "^0.8.1",
+ "grunt-ts": "^6.0.0-beta.22",
+ "gts": "^3.1.1",
+ "http-server": "^14.1.1",
+ "morgan": "^1.10.0",
+ "playwright-core": "^1.29.2",
+ "pngjs": "^6.0.0",
+ "portfinder": "^1.0.32",
+ "prettier": "~2.1.2",
+ "quiet-grunt": "^0.2.3",
+ "screenshot-ftw": "^1.0.5",
+ "serve-index": "^1.9.1",
+ "ts-node": "^9.0.0",
+ "typedoc": "^0.23.21",
+ "typescript": "~4.7.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
+ "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/cli": {
+ "version": "7.19.3",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz",
+ "integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.8",
+ "commander": "^4.0.1",
+ "convert-source-map": "^1.1.0",
+ "fs-readdir-recursive": "^1.1.0",
+ "glob": "^7.2.0",
+ "make-dir": "^2.1.0",
+ "slash": "^2.0.0"
+ },
+ "bin": {
+ "babel": "bin/babel.js",
+ "babel-external-helpers": "bin/babel-external-helpers.js"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "optionalDependencies": {
+ "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
+ "chokidar": "^3.4.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
+ "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
+ "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.20.5",
+ "@babel/helper-compilation-targets": "^7.20.0",
+ "@babel/helper-module-transforms": "^7.20.2",
+ "@babel/helpers": "^7.20.5",
+ "@babel/parser": "^7.20.5",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.5",
+ "@babel/types": "^7.20.5",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
+ "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.5",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+ "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.20.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
+ "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.20.0",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.21.3",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz",
+ "integrity": "sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-function-name": "^7.19.0",
+ "@babel/helper-member-expression-to-functions": "^7.18.9",
+ "@babel/helper-optimise-call-expression": "^7.18.6",
+ "@babel/helper-replace-supers": "^7.19.1",
+ "@babel/helper-split-export-declaration": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.18.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
+ "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.19.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
+ "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.18.10",
+ "@babel/types": "^7.19.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
+ "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.18.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz",
+ "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
+ "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.20.2",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.1",
+ "@babel/types": "^7.20.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz",
+ "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
+ "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz",
+ "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-member-expression-to-functions": "^7.18.9",
+ "@babel/helper-optimise-call-expression": "^7.18.6",
+ "@babel/traverse": "^7.19.1",
+ "@babel/types": "^7.19.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
+ "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
+ "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.19.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+ "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
+ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
+ "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.5",
+ "@babel/types": "^7.20.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
+ "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
+ "dev": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.20.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
+ "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.19.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz",
+ "integrity": "sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.20.2",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-typescript": "^7.20.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz",
+ "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/helper-validator-option": "^7.18.6",
+ "@babel/plugin-transform-typescript": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.18.10",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
+ "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/parser": "^7.18.10",
+ "@babel/types": "^7.18.10"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
+ "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.20.5",
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-function-name": "^7.19.0",
+ "@babel/helper-hoist-variables": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/parser": "^7.20.5",
+ "@babel/types": "^7.20.5",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
+ "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+ "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "node_modules/@nicolo-ribaudo/chokidar-2": {
+ "version": "2.1.8-no-fsevents.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
+ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.1.20",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz",
+ "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.14.2",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz",
+ "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.3.0"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/dom-mediacapture-transform": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.4.tgz",
+ "integrity": "sha512-G4DI51gU3zp/nCFVP7O5dv3sZ7nVXy3Dqooup8tDhvdzUNeAMiC0XIFGiwH3UHPh/t6L5odMOHwB3BYlY86WKw==",
+ "dev": true,
+ "dependencies": {
+ "@types/dom-webcodecs": "*"
+ }
+ },
+ "node_modules/@types/dom-webcodecs": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.5.tgz",
+ "integrity": "sha512-dsAE+4ws75W5mmNmIZ7IKZwv4bcz5GgPuA87u+Mk1CeVWB6g7ZwBfizRwBZDeyO12RSxoU3NlRa8jgLYQeSZGg==",
+ "dev": true
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.14",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
+ "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.18",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.28",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
+ "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "node_modules/@types/jquery": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
+ "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
+ "dev": true,
+ "dependencies": {
+ "@types/sizzle": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz",
+ "integrity": "sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
+ "dev": true
+ },
+ "node_modules/@types/minimist": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+ "dev": true
+ },
+ "node_modules/@types/morgan": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
+ "integrity": "sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "14.18.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
+ "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==",
+ "dev": true
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
+ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
+ "dev": true
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.0",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
+ "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==",
+ "dev": true
+ },
+ "node_modules/@types/pngjs": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
+ "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+ "dev": true
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.13.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
+ "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sizzle": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
+ "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^4.0.0",
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/experimental-utils": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
+ "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.7",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz",
+ "integrity": "sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/utils": "5.16.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
+ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true,
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.16.0.tgz",
+ "integrity": "sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.16.0",
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/typescript-estree": "5.16.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz",
+ "integrity": "sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/visitor-keys": "5.16.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.16.0.tgz",
+ "integrity": "sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz",
+ "integrity": "sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/visitor-keys": "5.16.0",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz",
+ "integrity": "sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.16.0",
+ "eslint-visitor-keys": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.25",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.25.tgz",
+ "integrity": "sha512-XTlMU1fEbVqIwuQAqlA0w8lJepW4KqeGmUxwWioVL0aoVgut0PE4ex+ixQWM74JKAyRfvS9+0lp+dFMfx5KZvw==",
+ "dev": true,
+ "dependencies": {
+ "typescript": "^4.6.4"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+ "dev": true
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz",
+ "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz",
+ "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+ "dev": true
+ },
+ "node_modules/async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "dev": true
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "dev": true,
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/babel-plugin-add-header-comment": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz",
+ "integrity": "sha1-URxJAQYmQNWkgLSsPt1pRBlYUOw=",
+ "dev": true
+ },
+ "node_modules/babel-plugin-const-enum": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-const-enum/-/babel-plugin-const-enum-1.2.0.tgz",
+ "integrity": "sha512-o1m/6iyyFnp9MRsK1dHF3bneqyf3AlM2q3A/YbgQr2pCat6B6XJVDv2TXqzfY2RYUi4mak6WAksSBPlyYGx9dg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-syntax-typescript": "^7.3.3",
+ "@babel/traverse": "^7.16.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "dev": true,
+ "dependencies": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/body-parser/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
+ "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001400",
+ "electron-to-chromium": "^1.4.251",
+ "node-releases": "^2.0.6",
+ "update-browserslist-db": "^1.0.9"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "dependencies": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-keys": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+ "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001435",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz",
+ "integrity": "sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "dependencies": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
+ "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+ "dev": true
+ },
+ "node_modules/copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csproj2ts": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-1.1.0.tgz",
+ "integrity": "sha512-sk0RTT51t4lUNQ7UfZrqjQx7q4g0m3iwNA6mvyh7gLsgQYvwKzfdyoAgicC9GqJvkoIkU0UmndV9c7VZ8pJ45Q==",
+ "dev": true,
+ "dependencies": {
+ "es6-promise": "^4.1.1",
+ "lodash": "^4.17.4",
+ "semver": "^5.4.1",
+ "xml2js": "^0.4.19"
+ }
+ },
+ "node_modules/csproj2ts/node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "dev": true
+ },
+ "node_modules/csproj2ts/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
+ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decamelize-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
+ "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
+ "dev": true,
+ "dependencies": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decamelize-keys/node_modules/map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "dev": true,
+ "dependencies": {
+ "repeating": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
+ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+ "dev": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.284",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
+ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es6-promise": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz",
+ "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+ "dev": true
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz",
+ "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
+ "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "resolve": "^1.20.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
+ "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "find-up": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-ban": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-ban/-/eslint-plugin-ban-1.6.0.tgz",
+ "integrity": "sha512-gZptoV+SFHOHO57/5lmPvizMvSXrjFatP9qlVQf3meL/WHo9TxSoERygrMlESl19CPh95U86asTxohT8OprwDw==",
+ "dev": true,
+ "dependencies": {
+ "requireindex": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-deprecation": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.3.3.tgz",
+ "integrity": "sha512-Bbkv6ZN2cCthVXz/oZKPwsSY5S/CbgTLRG4Q2s2gpPpgNsT0uJ0dB5oLNiWzFYY8AgKX4ULxXFG1l/rDav9QFA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "^5.0.0",
+ "tslib": "^2.3.1",
+ "tsutils": "^3.21.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "typescript": "^3.7.5 || ^4.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-es": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
+ "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+ "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.3",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.5",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/eslint-plugin-node": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
+ "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
+ "dev": true,
+ "dependencies": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.16.0"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz",
+ "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.0.0",
+ "prettier": ">=1.13.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint/node_modules/@babel/code-frame": {
+ "version": "7.12.11",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+ "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
+ "dev": true
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "dependencies": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/express/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/express/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/express/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "dependencies": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-diff": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-sync-cmp": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
+ "dev": true
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/finalhandler/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+ "dev": true,
+ "dependencies": {
+ "glob": "~5.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "dev": true,
+ "dependencies": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "dev": true
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "dev": true,
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "dependencies": {
+ "map-cache": "^0.2.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-readdir-recursive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+ "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
+ "dev": true
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/getobject": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz",
+ "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby/node_modules/ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/globby/node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
+ "dev": true
+ },
+ "node_modules/grunt": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz",
+ "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==",
+ "dev": true,
+ "dependencies": {
+ "dateformat": "~3.0.3",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.2",
+ "findup-sync": "~0.3.0",
+ "glob": "~7.1.6",
+ "grunt-cli": "~1.4.3",
+ "grunt-known-options": "~2.0.0",
+ "grunt-legacy-log": "~3.0.0",
+ "grunt-legacy-util": "~2.0.1",
+ "iconv-lite": "~0.4.13",
+ "js-yaml": "~3.14.0",
+ "minimatch": "~3.0.4",
+ "mkdirp": "~1.0.4",
+ "nopt": "~3.0.6",
+ "rimraf": "~3.0.2"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-cli": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz",
+ "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==",
+ "dev": true,
+ "dependencies": {
+ "grunt-known-options": "~2.0.0",
+ "interpret": "~1.1.0",
+ "liftup": "~3.0.1",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.2.0"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-cli/node_modules/nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/grunt-contrib-clean": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz",
+ "integrity": "sha512-uRvnXfhiZt8akb/ZRDHJpQQtkkVkqc/opWO4Po/9ehC2hPxgptB9S6JHDC/Nxswo4CJSM0iFPT/Iym3cEMWzKA==",
+ "dev": true,
+ "dependencies": {
+ "async": "^3.2.3",
+ "rimraf": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "grunt": ">=0.4.5"
+ }
+ },
+ "node_modules/grunt-contrib-clean/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/grunt-contrib-copy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+ "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^1.1.1",
+ "file-sync-cmp": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/grunt-known-options": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
+ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
+ "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "dev": true,
+ "dependencies": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
+ "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "~4.1.0",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
+ "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "dev": true,
+ "dependencies": {
+ "async": "~3.2.0",
+ "exit": "~0.1.2",
+ "getobject": "~1.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.21",
+ "underscore.string": "~3.3.5",
+ "which": "~2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-run": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/grunt-run/-/grunt-run-0.8.1.tgz",
+ "integrity": "sha512-+wvoOJevugcjMLldbVCyspRHHntwVIJiTGjx0HFq+UwXhVPe7AaAiUdY4135CS68pAoRLhd7pAILpL2ITe1tmA==",
+ "dev": true,
+ "dependencies": {
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "peerDependencies": {
+ "grunt": ">=0.4.0"
+ }
+ },
+ "node_modules/grunt-run/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-run/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts": {
+ "version": "6.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.22.tgz",
+ "integrity": "sha512-g9e+ZImQ7W38dfpwhp0+GUltXWidy3YGPfIA/IyGL5HMv6wmVmMMoSgscI5swhs2HSPf8yAvXAAJbwrouijoRg==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^2.0.4",
+ "csproj2ts": "^1.1.0",
+ "detect-indent": "^4.0.0",
+ "detect-newline": "^2.1.0",
+ "es6-promise": "~0.1.1",
+ "jsmin2": "^1.2.1",
+ "lodash": "~4.17.10",
+ "ncp": "0.5.1",
+ "rimraf": "2.2.6",
+ "semver": "^5.3.0",
+ "strip-bom": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "peerDependencies": {
+ "grunt": "^1.0.0 || ^0.4.0",
+ "typescript": ">=1"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "dependencies": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/anymatch/node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "optionalDependencies": {
+ "fsevents": "^1.2.7"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/glob-parent/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/micromatch/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/rimraf": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz",
+ "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=",
+ "dev": true,
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/grunt-ts/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt/node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/grunt/node_modules/minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/gts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/gts/-/gts-3.1.1.tgz",
+ "integrity": "sha512-Jw44aBbzMnd1vtZs7tZt3LMstKQukCBg7N4CKVGzviIQ45Cz5b9lxDJGXVKj/9ySuGv6TYEeijZJGbiiVcM27w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "^4.2.0",
+ "@typescript-eslint/parser": "^4.2.0",
+ "chalk": "^4.1.0",
+ "eslint": "^7.10.0",
+ "eslint-config-prettier": "^7.0.0",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^3.1.4",
+ "execa": "^5.0.0",
+ "inquirer": "^7.3.3",
+ "json5": "^2.1.3",
+ "meow": "^9.0.0",
+ "ncp": "^2.0.0",
+ "prettier": "^2.1.2",
+ "rimraf": "^3.0.2",
+ "write-file-atomic": "^3.0.3"
+ },
+ "bin": {
+ "gts": "build/src/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "typescript": ">=3"
+ }
+ },
+ "node_modules/gts/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/gts/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/gts/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/gts/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/gts/node_modules/ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
+ "dev": true,
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
+ "node_modules/gts/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hard-rejection": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
+ "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "dev": true,
+ "dependencies": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dev": true,
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-server/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/http-server/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/http-server/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/http-server/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/http-server/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/http-server/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/inquirer": {
+ "version": "7.3.3",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
+ "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.19",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.6.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/inquirer/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/inquirer/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+ "dev": true
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-descriptor/node_modules/kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finite": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
+ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true
+ },
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "dev": true
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jsmin2": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz",
+ "integrity": "sha1-iPvi+/dfCpH2YCD9mBzWk/S/5X4=",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonc-parser": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
+ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
+ "dev": true
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/liftup": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
+ "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
+ "dev": true,
+ "dependencies": {
+ "extend": "^3.0.2",
+ "findup-sync": "^4.0.0",
+ "fined": "^1.2.0",
+ "flagged-respawn": "^1.0.1",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.1",
+ "rechoir": "^0.7.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/liftup/node_modules/findup-sync": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+ "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^4.0.2",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lunr": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
+ "dev": true
+ },
+ "node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "node_modules/make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-obj": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "dev": true,
+ "dependencies": {
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/marked": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
+ "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==",
+ "dev": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/meow": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
+ "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/meow/node_modules/type-fest": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
+ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+ "dev": true
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minimist-options": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
+ "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+ "dev": true,
+ "dependencies": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "dev": true,
+ "dependencies": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mixin-deep/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "dev": true,
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "node_modules/nan": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
+ "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "node_modules/ncp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz",
+ "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=",
+ "dev": true,
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
+ "dev": true
+ },
+ "node_modules/nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+ "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "dev": true,
+ "dependencies": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+ "dev": true,
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+ "dev": true,
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
+ "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+ "dev": true,
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+ "dev": true,
+ "dependencies": {
+ "path-root-regex": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.29.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.29.2.tgz",
+ "integrity": "sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA==",
+ "dev": true,
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/portfinder": {
+ "version": "1.0.32",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
+ "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
+ "dev": true,
+ "dependencies": {
+ "async": "^2.6.4",
+ "debug": "^3.2.7",
+ "mkdirp": "^0.5.6"
+ },
+ "engines": {
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/portfinder/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/portfinder/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/portfinder/node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
+ "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/quick-lru": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
+ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/quiet-grunt": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/quiet-grunt/-/quiet-grunt-0.2.3.tgz",
+ "integrity": "sha1-8JCJeal9JCrC2NbuvP5Vj1nAYYQ=",
+ "dev": true
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+ "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/read-pkg/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-pkg/node_modules/type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
+ "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.9.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-not/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-not/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+ "dev": true
+ },
+ "node_modules/repeat-element": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
+ "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "dependencies": {
+ "is-finite": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "deprecated": "https://github.com/lydell/resolve-url#deprecated",
+ "dev": true
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/rxjs/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "dev": true,
+ "dependencies": {
+ "ret": "~0.1.10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "dev": true
+ },
+ "node_modules/screenshot-ftw": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/screenshot-ftw/-/screenshot-ftw-1.0.5.tgz",
+ "integrity": "sha512-LPKvVt9TBvUD9CEb1xolbtS3CJODwkcF0NxnxdyXwBiT+nLokLaxuuISNUMzWxekjVgYqx077mG1gNhkvIE1Mg==",
+ "dev": true
+ },
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
+ "dev": true
+ },
+ "node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/send/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/send/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/serve-index/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dev": true,
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shiki": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz",
+ "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==",
+ "dev": true,
+ "dependencies": {
+ "jsonc-parser": "^3.0.0",
+ "vscode-oniguruma": "^1.6.1",
+ "vscode-textmate": "^6.0.0"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "dev": true,
+ "dependencies": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
+ "dev": true,
+ "dependencies": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-url": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
+ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
+ "deprecated": "See https://github.com/lydell/source-map-url#deprecated",
+ "dev": true
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
+ "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
+ "dev": true
+ },
+ "node_modules/split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "node_modules/static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "dev": true,
+ "dependencies": {
+ "is-utf8": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
+ "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz",
+ "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-object-path/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/trim-newlines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
+ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
+ "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
+ "dev": true,
+ "dependencies": {
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.7"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
+ "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.1",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/typedoc": {
+ "version": "0.23.21",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.21.tgz",
+ "integrity": "sha512-VNE9Jv7BgclvyH9moi2mluneSviD43dCE9pY8RWkO88/DrEgJZk9KpUk7WO468c9WWs/+aG6dOnoH7ccjnErhg==",
+ "dev": true,
+ "dependencies": {
+ "lunr": "^2.3.9",
+ "marked": "^4.0.19",
+ "minimatch": "^5.1.0",
+ "shiki": "^0.11.1"
+ },
+ "bin": {
+ "typedoc": "bin/typedoc"
+ },
+ "engines": {
+ "node": ">= 14.14"
+ },
+ "peerDependencies": {
+ "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x"
+ }
+ },
+ "node_modules/typedoc/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/typedoc/node_modules/minimatch": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
+ "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
+ "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/underscore.string": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz",
+ "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "^1.1.1",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/underscore.string/node_modules/sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "node_modules/union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
+ "dev": true,
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "dev": true,
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "dev": true,
+ "dependencies": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "dev": true,
+ "dependencies": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "dependencies": {
+ "isarray": "1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
+ "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "browserslist-lint": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "deprecated": "Please see https://github.com/lydell/urix#deprecated",
+ "dev": true
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true
+ },
+ "node_modules/use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "node_modules/v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vscode-oniguruma": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
+ "dev": true
+ },
+ "node_modules/vscode-textmate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz",
+ "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==",
+ "dev": true
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dev": true,
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ },
+ "dependencies": {
+ "@ampproject/remapping": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
+ "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "^0.3.0"
+ }
+ },
+ "@babel/cli": {
+ "version": "7.19.3",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz",
+ "integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "^0.3.8",
+ "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
+ "chokidar": "^3.4.0",
+ "commander": "^4.0.1",
+ "convert-source-map": "^1.1.0",
+ "fs-readdir-recursive": "^1.1.0",
+ "glob": "^7.2.0",
+ "make-dir": "^2.1.0",
+ "slash": "^2.0.0"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ },
+ "@babel/compat-data": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
+ "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
+ "dev": true
+ },
+ "@babel/core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
+ "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
+ "dev": true,
+ "requires": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.20.5",
+ "@babel/helper-compilation-targets": "^7.20.0",
+ "@babel/helper-module-transforms": "^7.20.2",
+ "@babel/helpers": "^7.20.5",
+ "@babel/parser": "^7.20.5",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.5",
+ "@babel/types": "^7.20.5",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ }
+ },
+ "@babel/generator": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
+ "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.20.5",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "jsesc": "^2.5.1"
+ }
+ },
+ "@babel/helper-annotate-as-pure": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+ "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-compilation-targets": {
+ "version": "7.20.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
+ "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
+ "dev": true,
+ "requires": {
+ "@babel/compat-data": "^7.20.0",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.21.3",
+ "semver": "^6.3.0"
+ }
+ },
+ "@babel/helper-create-class-features-plugin": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz",
+ "integrity": "sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-function-name": "^7.19.0",
+ "@babel/helper-member-expression-to-functions": "^7.18.9",
+ "@babel/helper-optimise-call-expression": "^7.18.6",
+ "@babel/helper-replace-supers": "^7.19.1",
+ "@babel/helper-split-export-declaration": "^7.18.6"
+ }
+ },
+ "@babel/helper-environment-visitor": {
+ "version": "7.18.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
+ "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+ "dev": true
+ },
+ "@babel/helper-function-name": {
+ "version": "7.19.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
+ "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.18.10",
+ "@babel/types": "^7.19.0"
+ }
+ },
+ "@babel/helper-hoist-variables": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
+ "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-member-expression-to-functions": {
+ "version": "7.18.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz",
+ "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.9"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-module-transforms": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
+ "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.20.2",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.1",
+ "@babel/types": "^7.20.2"
+ }
+ },
+ "@babel/helper-optimise-call-expression": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz",
+ "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-plugin-utils": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
+ "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
+ "dev": true
+ },
+ "@babel/helper-replace-supers": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz",
+ "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-member-expression-to-functions": "^7.18.9",
+ "@babel/helper-optimise-call-expression": "^7.18.6",
+ "@babel/traverse": "^7.19.1",
+ "@babel/types": "^7.19.0"
+ }
+ },
+ "@babel/helper-simple-access": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
+ "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.20.2"
+ }
+ },
+ "@babel/helper-split-export-declaration": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
+ "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-string-parser": {
+ "version": "7.19.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+ "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "dev": true
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "dev": true
+ },
+ "@babel/helper-validator-option": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
+ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "dev": true
+ },
+ "@babel/helpers": {
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
+ "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.18.10",
+ "@babel/traverse": "^7.20.5",
+ "@babel/types": "^7.20.5"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@babel/parser": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
+ "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
+ "dev": true
+ },
+ "@babel/plugin-syntax-typescript": {
+ "version": "7.20.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
+ "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.19.0"
+ }
+ },
+ "@babel/plugin-transform-typescript": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz",
+ "integrity": "sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-class-features-plugin": "^7.20.2",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-typescript": "^7.20.0"
+ }
+ },
+ "@babel/preset-typescript": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz",
+ "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/helper-validator-option": "^7.18.6",
+ "@babel/plugin-transform-typescript": "^7.18.6"
+ }
+ },
+ "@babel/template": {
+ "version": "7.18.10",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
+ "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/parser": "^7.18.10",
+ "@babel/types": "^7.18.10"
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
+ "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.20.5",
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-function-name": "^7.19.0",
+ "@babel/helper-hoist-variables": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/parser": "^7.20.5",
+ "@babel/types": "^7.20.5",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ }
+ },
+ "@babel/types": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
+ "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@eslint/eslintrc": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true
+ }
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true
+ },
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+ "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "@nicolo-ribaudo/chokidar-2": {
+ "version": "2.1.8-no-fsevents.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
+ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@types/babel__core": {
+ "version": "7.1.20",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz",
+ "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "@types/babel__generator": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "@types/babel__template": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "@types/babel__traverse": {
+ "version": "7.14.2",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz",
+ "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.3.0"
+ }
+ },
+ "@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dev": true,
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/dom-mediacapture-transform": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.4.tgz",
+ "integrity": "sha512-G4DI51gU3zp/nCFVP7O5dv3sZ7nVXy3Dqooup8tDhvdzUNeAMiC0XIFGiwH3UHPh/t6L5odMOHwB3BYlY86WKw==",
+ "dev": true,
+ "requires": {
+ "@types/dom-webcodecs": "*"
+ }
+ },
+ "@types/dom-webcodecs": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.5.tgz",
+ "integrity": "sha512-dsAE+4ws75W5mmNmIZ7IKZwv4bcz5GgPuA87u+Mk1CeVWB6g7ZwBfizRwBZDeyO12RSxoU3NlRa8jgLYQeSZGg==",
+ "dev": true
+ },
+ "@types/express": {
+ "version": "4.17.14",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
+ "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+ "dev": true,
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.18",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.17.28",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
+ "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "@types/jquery": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
+ "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
+ "dev": true,
+ "requires": {
+ "@types/sizzle": "*"
+ }
+ },
+ "@types/json-schema": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz",
+ "integrity": "sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A==",
+ "dev": true
+ },
+ "@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
+ "dev": true
+ },
+ "@types/minimist": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+ "dev": true
+ },
+ "@types/morgan": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
+ "integrity": "sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/node": {
+ "version": "14.18.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
+ "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==",
+ "dev": true
+ },
+ "@types/normalize-package-data": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
+ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
+ "dev": true
+ },
+ "@types/offscreencanvas": {
+ "version": "2019.7.0",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
+ "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==",
+ "dev": true
+ },
+ "@types/pngjs": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
+ "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
+ },
+ "@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+ "dev": true
+ },
+ "@types/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/serve-static": {
+ "version": "1.13.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
+ "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
+ "dev": true,
+ "requires": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "@types/sizzle": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "dev": true
+ },
+ "@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
+ "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
+ "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.7",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/experimental-utils": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz",
+ "integrity": "sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/utils": "5.16.0"
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
+ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/utils": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.16.0.tgz",
+ "integrity": "sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.16.0",
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/typescript-estree": "5.16.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz",
+ "integrity": "sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/visitor-keys": "5.16.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.16.0.tgz",
+ "integrity": "sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz",
+ "integrity": "sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.16.0",
+ "@typescript-eslint/visitor-keys": "5.16.0",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz",
+ "integrity": "sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.16.0",
+ "eslint-visitor-keys": "^3.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "@webgpu/types": {
+ "version": "0.1.25",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.25.tgz",
+ "integrity": "sha512-XTlMU1fEbVqIwuQAqlA0w8lJepW4KqeGmUxwWioVL0aoVgut0PE4ex+ixQWM74JKAyRfvS9+0lp+dFMfx5KZvw==",
+ "dev": true,
+ "requires": {
+ "typescript": "^4.6.4"
+ }
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "requires": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ }
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.21.3"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true
+ },
+ "arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "dev": true
+ },
+ "arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "dev": true
+ },
+ "array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
+ "dev": true
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+ "dev": true
+ },
+ "array-includes": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz",
+ "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true
+ },
+ "array.prototype.flat": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz",
+ "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0"
+ }
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true
+ },
+ "assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true
+ },
+ "async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
+ "dev": true
+ },
+ "async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "dev": true
+ },
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "dev": true
+ },
+ "babel-plugin-add-header-comment": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz",
+ "integrity": "sha1-URxJAQYmQNWkgLSsPt1pRBlYUOw=",
+ "dev": true
+ },
+ "babel-plugin-const-enum": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-const-enum/-/babel-plugin-const-enum-1.2.0.tgz",
+ "integrity": "sha512-o1m/6iyyFnp9MRsK1dHF3bneqyf3AlM2q3A/YbgQr2pCat6B6XJVDv2TXqzfY2RYUi4mak6WAksSBPlyYGx9dg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-syntax-typescript": "^7.3.3",
+ "@babel/traverse": "^7.16.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "dev": true,
+ "requires": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dev": true,
+ "requires": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ }
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browserslist": {
+ "version": "4.21.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
+ "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001400",
+ "electron-to-chromium": "^1.4.251",
+ "node-releases": "^2.0.6",
+ "update-browserslist-db": "^1.0.9"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true
+ },
+ "cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "requires": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ }
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "camelcase-keys": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+ "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ }
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001435",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz",
+ "integrity": "sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ }
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "dev": true
+ },
+ "collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "requires": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+ "dev": true
+ },
+ "component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "5.2.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ }
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
+ "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "dev": true
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+ "dev": true
+ },
+ "copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true
+ },
+ "core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
+ "dev": true
+ },
+ "create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "csproj2ts": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-1.1.0.tgz",
+ "integrity": "sha512-sk0RTT51t4lUNQ7UfZrqjQx7q4g0m3iwNA6mvyh7gLsgQYvwKzfdyoAgicC9GqJvkoIkU0UmndV9c7VZ8pJ45Q==",
+ "dev": true,
+ "requires": {
+ "es6-promise": "^4.1.1",
+ "lodash": "^4.17.4",
+ "semver": "^5.4.1",
+ "xml2js": "^0.4.19"
+ },
+ "dependencies": {
+ "es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "dateformat": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
+ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "decamelize-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
+ "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
+ "dev": true,
+ "requires": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ },
+ "dependencies": {
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "dev": true
+ }
+ }
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "dev": true
+ },
+ "destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+ "dev": true
+ },
+ "detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "detect-newline": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
+ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
+ "dev": true
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+ "dev": true
+ },
+ "electron-to-chromium": {
+ "version": "1.4.284",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
+ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "dev": true
+ },
+ "enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^4.1.1"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "es6-promise": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz",
+ "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=",
+ "dev": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.12.11",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+ "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "globals": {
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz",
+ "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-config-prettier": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz",
+ "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==",
+ "dev": true,
+ "requires": {}
+ },
+ "eslint-import-resolver-node": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
+ "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "resolve": "^1.20.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-module-utils": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
+ "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "find-up": "^2.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-plugin-ban": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-ban/-/eslint-plugin-ban-1.6.0.tgz",
+ "integrity": "sha512-gZptoV+SFHOHO57/5lmPvizMvSXrjFatP9qlVQf3meL/WHo9TxSoERygrMlESl19CPh95U86asTxohT8OprwDw==",
+ "dev": true,
+ "requires": {
+ "requireindex": "~1.2.0"
+ }
+ },
+ "eslint-plugin-deprecation": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.3.3.tgz",
+ "integrity": "sha512-Bbkv6ZN2cCthVXz/oZKPwsSY5S/CbgTLRG4Q2s2gpPpgNsT0uJ0dB5oLNiWzFYY8AgKX4ULxXFG1l/rDav9QFA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "^5.0.0",
+ "tslib": "^2.3.1",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "eslint-plugin-es": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
+ "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
+ "dev": true,
+ "requires": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ }
+ },
+ "eslint-plugin-import": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+ "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.3",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.5",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-node": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
+ "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
+ "dev": true,
+ "requires": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-prettier": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz",
+ "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==",
+ "dev": true,
+ "requires": {
+ "prettier-linter-helpers": "^1.0.0"
+ }
+ },
+ "eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true
+ },
+ "espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ }
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ }
+ }
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true
+ },
+ "eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
+ "dev": true
+ },
+ "eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true
+ },
+ "execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+ "dev": true
+ },
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "requires": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dev": true,
+ "requires": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "requires": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "fast-diff": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "file-sync-cmp": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
+ "dev": true
+ },
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true,
+ "optional": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true
+ }
+ }
+ },
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ },
+ "findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+ "dev": true,
+ "requires": {
+ "glob": "~5.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "dev": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ }
+ },
+ "flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "dev": true
+ },
+ "follow-redirects": {
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "dev": true
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true
+ },
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true
+ },
+ "fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "requires": {
+ "map-cache": "^0.2.2"
+ }
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true
+ },
+ "fs-readdir-recursive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+ "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+ "dev": true
+ },
+ "getobject": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz",
+ "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "requires": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ }
+ },
+ "global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "dependencies": {
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
+ },
+ "globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "requires": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ },
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ }
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
+ "dev": true
+ },
+ "grunt": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz",
+ "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==",
+ "dev": true,
+ "requires": {
+ "dateformat": "~3.0.3",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.2",
+ "findup-sync": "~0.3.0",
+ "glob": "~7.1.6",
+ "grunt-cli": "~1.4.3",
+ "grunt-known-options": "~2.0.0",
+ "grunt-legacy-log": "~3.0.0",
+ "grunt-legacy-util": "~2.0.1",
+ "iconv-lite": "~0.4.13",
+ "js-yaml": "~3.14.0",
+ "minimatch": "~3.0.4",
+ "mkdirp": "~1.0.4",
+ "nopt": "~3.0.6",
+ "rimraf": "~3.0.2"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ }
+ }
+ },
+ "grunt-cli": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz",
+ "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==",
+ "dev": true,
+ "requires": {
+ "grunt-known-options": "~2.0.0",
+ "interpret": "~1.1.0",
+ "liftup": "~3.0.1",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.2.0"
+ },
+ "dependencies": {
+ "nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ }
+ }
+ },
+ "grunt-contrib-clean": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz",
+ "integrity": "sha512-uRvnXfhiZt8akb/ZRDHJpQQtkkVkqc/opWO4Po/9ehC2hPxgptB9S6JHDC/Nxswo4CJSM0iFPT/Iym3cEMWzKA==",
+ "dev": true,
+ "requires": {
+ "async": "^3.2.3",
+ "rimraf": "^2.6.2"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "grunt-contrib-copy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+ "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.1",
+ "file-sync-cmp": "^0.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
+ },
+ "grunt-known-options": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
+ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
+ "dev": true
+ },
+ "grunt-legacy-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
+ "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "dev": true,
+ "requires": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.19"
+ }
+ },
+ "grunt-legacy-log-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
+ "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "dev": true,
+ "requires": {
+ "chalk": "~4.1.0",
+ "lodash": "~4.17.19"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "grunt-legacy-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
+ "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "dev": true,
+ "requires": {
+ "async": "~3.2.0",
+ "exit": "~0.1.2",
+ "getobject": "~1.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.21",
+ "underscore.string": "~3.3.5",
+ "which": "~2.0.2"
+ }
+ },
+ "grunt-run": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/grunt-run/-/grunt-run-0.8.1.tgz",
+ "integrity": "sha512-+wvoOJevugcjMLldbVCyspRHHntwVIJiTGjx0HFq+UwXhVPe7AaAiUdY4135CS68pAoRLhd7pAILpL2ITe1tmA==",
+ "dev": true,
+ "requires": {
+ "strip-ansi": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
+ }
+ },
+ "grunt-ts": {
+ "version": "6.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.22.tgz",
+ "integrity": "sha512-g9e+ZImQ7W38dfpwhp0+GUltXWidy3YGPfIA/IyGL5HMv6wmVmMMoSgscI5swhs2HSPf8yAvXAAJbwrouijoRg==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^2.0.4",
+ "csproj2ts": "^1.1.0",
+ "detect-indent": "^4.0.0",
+ "detect-newline": "^2.1.0",
+ "es6-promise": "~0.1.1",
+ "jsmin2": "^1.2.1",
+ "lodash": "~4.17.10",
+ "ncp": "0.5.1",
+ "rimraf": "2.2.6",
+ "semver": "^5.3.0",
+ "strip-bom": "^2.0.0"
+ },
+ "dependencies": {
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ },
+ "dependencies": {
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ }
+ }
+ },
+ "binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ }
+ },
+ "chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "dev": true,
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ }
+ },
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ }
+ },
+ "fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ }
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ },
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ }
+ }
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz",
+ "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "gts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/gts/-/gts-3.1.1.tgz",
+ "integrity": "sha512-Jw44aBbzMnd1vtZs7tZt3LMstKQukCBg7N4CKVGzviIQ45Cz5b9lxDJGXVKj/9ySuGv6TYEeijZJGbiiVcM27w==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/eslint-plugin": "^4.2.0",
+ "@typescript-eslint/parser": "^4.2.0",
+ "chalk": "^4.1.0",
+ "eslint": "^7.10.0",
+ "eslint-config-prettier": "^7.0.0",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^3.1.4",
+ "execa": "^5.0.0",
+ "inquirer": "^7.3.3",
+ "json5": "^2.1.3",
+ "meow": "^9.0.0",
+ "ncp": "^2.0.0",
+ "prettier": "^2.1.2",
+ "rimraf": "^3.0.2",
+ "write-file-atomic": "^3.0.3"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "hard-rejection": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
+ "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ }
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "dependencies": {
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=",
+ "dev": true
+ },
+ "hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "requires": {
+ "whatwg-encoding": "^2.0.0"
+ }
+ },
+ "http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dev": true,
+ "requires": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true
+ }
+ }
+ },
+ "http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "requires": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "requires": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "7.3.3",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
+ "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.19",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.6.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+ "dev": true
+ },
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true
+ },
+ "is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "requires": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-finite": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
+ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
+ "is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "requires": {
+ "is-unc-path": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true
+ },
+ "is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "requires": {
+ "unc-path-regex": "^0.1.2"
+ }
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "dev": true
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true
+ },
+ "jsmin2": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz",
+ "integrity": "sha1-iPvi+/dfCpH2YCD9mBzWk/S/5X4=",
+ "dev": true
+ },
+ "json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "json5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+ "dev": true
+ },
+ "jsonc-parser": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
+ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
+ "dev": true
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "liftup": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
+ "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
+ "dev": true,
+ "requires": {
+ "extend": "^3.0.2",
+ "findup-sync": "^4.0.0",
+ "fined": "^1.2.0",
+ "flagged-respawn": "^1.0.1",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.1",
+ "rechoir": "^0.7.0",
+ "resolve": "^1.19.0"
+ },
+ "dependencies": {
+ "findup-sync": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+ "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^4.0.2",
+ "resolve-dir": "^1.0.1"
+ }
+ }
+ }
+ },
+ "lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "lunr": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
+ "dev": true
+ },
+ "make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "requires": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
+ "map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+ "dev": true
+ },
+ "map-obj": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+ "dev": true
+ },
+ "map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "dev": true,
+ "requires": {
+ "object-visit": "^1.0.0"
+ }
+ },
+ "marked": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
+ "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==",
+ "dev": true
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true
+ },
+ "meow": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
+ "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+ "dev": true,
+ "requires": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
+ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+ "dev": true
+ }
+ }
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+ "dev": true
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+ "dev": true
+ },
+ "minimist-options": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
+ "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+ "dev": true,
+ "requires": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ }
+ },
+ "mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "dev": true,
+ "requires": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "nan": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
+ "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
+ "dev": true,
+ "optional": true
+ },
+ "nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ }
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ },
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "ncp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz",
+ "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=",
+ "dev": true
+ },
+ "negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true
+ },
+ "node-releases": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
+ "dev": true
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-package-data": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+ "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "dev": true,
+ "requires": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "object-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+ "dev": true,
+ "requires": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+ "dev": true,
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "object.values": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
+ "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "dev": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+ "dev": true,
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ }
+ },
+ "parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+ "dev": true
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true
+ },
+ "pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true
+ },
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+ "dev": true,
+ "requires": {
+ "path-root-regex": "^0.1.0"
+ }
+ },
+ "path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+ "dev": true
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true
+ },
+ "playwright-core": {
+ "version": "1.29.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.29.2.tgz",
+ "integrity": "sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA==",
+ "dev": true
+ },
+ "pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "dev": true
+ },
+ "portfinder": {
+ "version": "1.0.32",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
+ "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
+ "dev": true,
+ "requires": {
+ "async": "^2.6.4",
+ "debug": "^3.2.7",
+ "mkdirp": "^0.5.6"
+ },
+ "dependencies": {
+ "async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.6"
+ }
+ }
+ }
+ },
+ "posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
+ "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
+ "dev": true
+ },
+ "prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "requires": {
+ "fast-diff": "^1.1.2"
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
+ "proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "requires": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true
+ },
+ "quick-lru": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
+ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+ "dev": true
+ },
+ "quiet-grunt": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/quiet-grunt/-/quiet-grunt-0.2.3.tgz",
+ "integrity": "sha1-8JCJeal9JCrC2NbuvP5Vj1nAYYQ=",
+ "dev": true
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true
+ },
+ "raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dev": true,
+ "requires": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "requires": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "dependencies": {
+ "hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true
+ }
+ }
+ },
+ "read-pkg-up": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+ "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+ "dev": true,
+ "requires": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "rechoir": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
+ "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+ "dev": true,
+ "requires": {
+ "resolve": "^1.9.0"
+ }
+ },
+ "redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "requires": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ }
+ },
+ "regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ }
+ },
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true
+ },
+ "remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+ "dev": true
+ },
+ "repeat-element": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
+ "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==",
+ "dev": true
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true
+ },
+ "requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true
+ },
+ "requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ }
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "dev": true,
+ "requires": {
+ "ret": "~0.1.10"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "dev": true
+ },
+ "screenshot-ftw": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/screenshot-ftw/-/screenshot-ftw-1.0.5.tgz",
+ "integrity": "sha512-LPKvVt9TBvUD9CEb1xolbtS3CJODwkcF0NxnxdyXwBiT+nLokLaxuuISNUMzWxekjVgYqx077mG1gNhkvIE1Mg==",
+ "dev": true
+ },
+ "secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
+ "dev": true
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ }
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true
+ }
+ }
+ },
+ "serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+ "dev": true,
+ "requires": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dev": true,
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ }
+ },
+ "set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "shiki": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz",
+ "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==",
+ "dev": true,
+ "requires": {
+ "jsonc-parser": "^3.0.0",
+ "vscode-oniguruma": "^1.6.1",
+ "vscode-textmate": "^6.0.0"
+ }
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
+ },
+ "snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "dev": true,
+ "requires": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.2.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "dev": true,
+ "requires": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "source-map-url": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
+ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
+ "dev": true
+ },
+ "spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
+ "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
+ "dev": true
+ },
+ "split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ }
+ },
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "dev": true,
+ "requires": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ }
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "dev": true
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "dev": true,
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true
+ },
+ "strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "requires": {
+ "min-indent": "^1.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "table": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
+ "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "dev": true,
+ "requires": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz",
+ "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ }
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+ "dev": true
+ },
+ "to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ }
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ },
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true
+ },
+ "trim-newlines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
+ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+ "dev": true
+ },
+ "ts-node": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
+ "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
+ "dev": true,
+ "requires": {
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ }
+ },
+ "tsconfig-paths": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
+ "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
+ "dev": true,
+ "requires": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.1",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true
+ }
+ }
+ },
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "dev": true
+ },
+ "tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ }
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "typedoc": {
+ "version": "0.23.21",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.21.tgz",
+ "integrity": "sha512-VNE9Jv7BgclvyH9moi2mluneSviD43dCE9pY8RWkO88/DrEgJZk9KpUk7WO468c9WWs/+aG6dOnoH7ccjnErhg==",
+ "dev": true,
+ "requires": {
+ "lunr": "^2.3.9",
+ "marked": "^4.0.19",
+ "minimatch": "^5.1.0",
+ "shiki": "^0.11.1"
+ },
+ "dependencies": {
+ "brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
+ "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^2.0.1"
+ }
+ }
+ }
+ },
+ "typescript": {
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
+ "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "dev": true
+ },
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+ "dev": true
+ },
+ "underscore.string": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz",
+ "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "^1.1.1",
+ "util-deprecate": "^1.0.2"
+ },
+ "dependencies": {
+ "sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ }
+ }
+ },
+ "union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
+ "dev": true,
+ "requires": {
+ "qs": "^6.4.0"
+ }
+ },
+ "union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true
+ },
+ "unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "dev": true,
+ "requires": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "dependencies": {
+ "has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "dependencies": {
+ "isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "requires": {
+ "isarray": "1.0.0"
+ }
+ }
+ }
+ },
+ "has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+ "dev": true
+ }
+ }
+ },
+ "upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "dev": true
+ },
+ "update-browserslist-db": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
+ "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "dev": true,
+ "requires": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "dev": true
+ },
+ "url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true
+ },
+ "use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "dev": true
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "dev": true
+ },
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "dev": true
+ },
+ "vscode-oniguruma": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
+ "dev": true
+ },
+ "vscode-textmate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz",
+ "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==",
+ "dev": true
+ },
+ "whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.6.3"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ }
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "requires": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dev": true,
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ }
+ },
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/package.json b/dom/webgpu/tests/cts/checkout/package.json
new file mode 100644
index 0000000000..8811267db3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@webgpu/cts",
+ "version": "0.1.0",
+ "description": "WebGPU Conformance Test Suite",
+ "scripts": {
+ "test": "grunt pre",
+ "check": "grunt check",
+ "standalone": "grunt standalone",
+ "wpt": "grunt wpt",
+ "fix": "grunt fix",
+ "unittest": "grunt unittest",
+ "gen_wpt_cts_html": "node tools/gen_wpt_cts_html",
+ "gen_cache": "node tools/gen_cache",
+ "tsdoc": "grunt run:tsdoc",
+ "start": "node tools/dev_server",
+ "dev": "node tools/dev_server"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/gpuweb/cts.git"
+ },
+ "author": "WebGPU CTS Contributors",
+ "private": true,
+ "license": "BSD-3-Clause",
+ "bugs": {
+ "url": "https://github.com/gpuweb/cts/issues"
+ },
+ "homepage": "https://github.com/gpuweb/cts#readme",
+ "devDependencies": {
+ "@babel/cli": "^7.19.3",
+ "@babel/core": "^7.20.5",
+ "@babel/preset-typescript": "^7.18.6",
+ "@types/babel__core": "^7.1.20",
+ "@types/dom-mediacapture-transform": "^0.1.4",
+ "@types/dom-webcodecs": "^0.1.5",
+ "@types/express": "^4.17.14",
+ "@types/jquery": "^3.5.14",
+ "@types/morgan": "^1.9.3",
+ "@types/node": "^14.18.12",
+ "@types/offscreencanvas": "^2019.7.0",
+ "@types/pngjs": "^6.0.1",
+ "@types/serve-index": "^1.9.1",
+ "@typescript-eslint/parser": "^4.33.0",
+ "@webgpu/types": "0.1.25",
+ "ansi-colors": "4.1.1",
+ "babel-plugin-add-header-comment": "^1.0.3",
+ "babel-plugin-const-enum": "^1.2.0",
+ "chokidar": "^3.5.3",
+ "eslint": "^7.11.0",
+ "eslint-plugin-ban": "^1.6.0",
+ "eslint-plugin-deprecation": "^1.3.3",
+ "eslint-plugin-import": "^2.26.0",
+ "express": "^4.18.2",
+ "grunt": "^1.5.3",
+ "grunt-cli": "^1.4.3",
+ "grunt-contrib-clean": "^2.0.1",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-run": "^0.8.1",
+ "grunt-ts": "^6.0.0-beta.22",
+ "gts": "^3.1.1",
+ "http-server": "^14.1.1",
+ "morgan": "^1.10.0",
+ "playwright-core": "^1.29.2",
+ "pngjs": "^6.0.0",
+ "portfinder": "^1.0.32",
+ "prettier": "~2.1.2",
+ "quiet-grunt": "^0.2.3",
+ "screenshot-ftw": "^1.0.5",
+ "serve-index": "^1.9.1",
+ "ts-node": "^9.0.0",
+ "typedoc": "^0.23.21",
+ "typescript": "~4.7.4"
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/prettier.config.js b/dom/webgpu/tests/cts/checkout/prettier.config.js
new file mode 100644
index 0000000000..9f4053f719
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/prettier.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ printWidth: 100,
+
+ arrowParens: 'avoid',
+ bracketSpacing: true,
+ singleQuote: true,
+ trailingComma: 'es5',
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts
new file mode 100644
index 0000000000..6f6e80288a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts
@@ -0,0 +1,120 @@
+/**
+ * Utilities to improve the performance of the CTS, by caching data that is
+ * expensive to build using a two-level cache (in-memory, pre-computed file).
+ */
+
+interface DataStore {
+ load(path: string): Promise<string>;
+}
+
+/** Logger is a basic debug logger function */
+export type Logger = (s: string) => void;
+
+/** DataCache is an interface to a data store used to hold cached data */
+export class DataCache {
+ /** setDataStore() sets the backing data store used by the data cache */
+ public setStore(dataStore: DataStore) {
+ this.dataStore = dataStore;
+ }
+
+ /** setDebugLogger() sets the verbose logger */
+ public setDebugLogger(logger: Logger) {
+ this.debugLogger = logger;
+ }
+
+ /**
+ * fetch() retrieves cacheable data from the data cache, first checking the
+ * in-memory cache, then the data store (if specified), then resorting to
+ * building the data and storing it in the cache.
+ */
+ public async fetch<Data>(cacheable: Cacheable<Data>): Promise<Data> {
+ // First check the in-memory cache
+ let data = this.cache.get(cacheable.path);
+ if (data !== undefined) {
+ this.log('in-memory cache hit');
+ return Promise.resolve(data as Data);
+ }
+ this.log('in-memory cache miss');
+ // In in-memory cache miss.
+ // Next, try the data store.
+ if (this.dataStore !== null && !this.unavailableFiles.has(cacheable.path)) {
+ let serialized: string | undefined;
+ try {
+ serialized = await this.dataStore.load(cacheable.path);
+ this.log('loaded serialized');
+ } catch (err) {
+ // not found in data store
+ this.log(`failed to load (${cacheable.path}): ${err}`);
+ this.unavailableFiles.add(cacheable.path);
+ }
+ if (serialized !== undefined) {
+ this.log(`deserializing`);
+ data = cacheable.deserialize(serialized);
+ this.cache.set(cacheable.path, data);
+ return data as Data;
+ }
+ }
+ // Not found anywhere. Build the data, and cache for future lookup.
+ this.log(`cache: building (${cacheable.path})`);
+ data = await cacheable.build();
+ this.cache.set(cacheable.path, data);
+ return data as Data;
+ }
+
+ private log(msg: string) {
+ if (this.debugLogger !== null) {
+ this.debugLogger(`DataCache: ${msg}`);
+ }
+ }
+
+ private cache = new Map<string, unknown>();
+ private unavailableFiles = new Set<string>();
+ private dataStore: DataStore | null = null;
+ private debugLogger: Logger | null = null;
+}
+
+/** The data cache */
+export const dataCache = new DataCache();
+
+/** true if the current process is building the cache */
+let isBuildingDataCache = false;
+
+/** @returns true if the data cache is currently being built */
+export function getIsBuildingDataCache() {
+ return isBuildingDataCache;
+}
+
+/** Sets whether the data cache is currently being built */
+export function setIsBuildingDataCache(value = true) {
+ isBuildingDataCache = value;
+}
+
+/**
+ * Cacheable is the interface to something that can be stored into the
+ * DataCache.
+ * The 'npm run gen_cache' tool will look for module-scope variables of this
+ * interface, with the name `d`.
+ */
+export interface Cacheable<Data> {
+ /** the globally unique path for the cacheable data */
+ readonly path: string;
+
+ /**
+ * build() builds the cacheable data.
+ * This is assumed to be an expensive operation and will only happen if the
+ * cache does not already contain the built data.
+ */
+ build(): Promise<Data>;
+
+ /**
+ * serialize() transforms `data` to a string (usually JSON encoded) so that it
+ * can be stored in a text cache file.
+ */
+ serialize(data: Data): string;
+
+ /**
+ * deserialize() is the inverse of serialize(), transforming the string back
+ * to the Data object.
+ */
+ deserialize(serialized: string): Data;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts
new file mode 100644
index 0000000000..1368a3f96e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts
@@ -0,0 +1,328 @@
+import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
+import { JSONWithUndefined } from '../internal/params_utils.js';
+import { assert, unreachable } from '../util/util.js';
+
+export class SkipTestCase extends Error {}
+export class UnexpectedPassError extends Error {}
+
+export { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
+
+/** The fully-general type for params passed to a test function invocation. */
+export type TestParams = {
+ readonly [k: string]: JSONWithUndefined;
+};
+
+type DestroyableObject =
+ | { destroy(): void }
+ | { close(): void }
+ | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context };
+
+export class SubcaseBatchState {
+ private _params: TestParams;
+
+ constructor(params: TestParams) {
+ this._params = params;
+ }
+
+ /**
+ * Returns the case parameters for this test fixture shared state. Subcase params
+ * are not included.
+ */
+ get params(): TestParams {
+ return this._params;
+ }
+
+ /**
+ * Runs before the `.before()` function.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async init() {}
+ /**
+ * Runs between the `.before()` function and the subcases.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async postInit() {}
+ /**
+ * Runs after all subcases finish.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async finalize() {}
+}
+
+/**
+ * A Fixture is a class used to instantiate each test sub/case at run time.
+ * A new instance of the Fixture is created for every single test subcase
+ * (i.e. every time the test function is run).
+ */
+export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
+ private _params: unknown;
+ private _sharedState: S;
+ /**
+ * Interface for recording logs and test status.
+ *
+ * @internal
+ */
+ protected rec: TestCaseRecorder;
+ private eventualExpectations: Array<Promise<unknown>> = [];
+ private numOutstandingAsyncExpectations = 0;
+ private objectsToCleanUp: DestroyableObject[] = [];
+
+ public static MakeSharedState(params: TestParams): SubcaseBatchState {
+ return new SubcaseBatchState(params);
+ }
+
+ /** @internal */
+ constructor(sharedState: S, rec: TestCaseRecorder, params: TestParams) {
+ this._sharedState = sharedState;
+ this.rec = rec;
+ this._params = params;
+ }
+
+ /**
+ * Returns the (case+subcase) parameters for this test function invocation.
+ */
+ get params(): unknown {
+ return this._params;
+ }
+
+ /**
+ * Gets the test fixture's shared state. This object is shared between subcases
+ * within the same testcase.
+ */
+ get sharedState(): S {
+ return this._sharedState;
+ }
+
+ /**
+ * Override this to do additional pre-test-function work in a derived fixture.
+ * This has to be a member function instead of an async `createFixture` function, because
+ * we need to be able to ergonomically override it in subclasses.
+ *
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async init(): Promise<void> {}
+
+ /**
+ * Override this to do additional post-test-function work in a derived fixture.
+ *
+ * Called even if init was unsuccessful.
+ *
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async finalize(): Promise<void> {
+ assert(
+ this.numOutstandingAsyncExpectations === 0,
+ 'there were outstanding immediateAsyncExpectations (e.g. expectUncapturedError) at the end of the test'
+ );
+
+ // Loop to exhaust the eventualExpectations in case they chain off each other.
+ while (this.eventualExpectations.length) {
+ const p = this.eventualExpectations.shift()!;
+ try {
+ await p;
+ } catch (ex) {
+ this.rec.threw(ex);
+ }
+ }
+
+ // And clean up any objects now that they're done being used.
+ for (const o of this.objectsToCleanUp) {
+ if ('getExtension' in o) {
+ const WEBGL_lose_context = o.getExtension('WEBGL_lose_context');
+ if (WEBGL_lose_context) WEBGL_lose_context.loseContext();
+ } else if ('destroy' in o) {
+ o.destroy();
+ } else {
+ o.close();
+ }
+ }
+ }
+
+ /**
+ * Tracks an object to be cleaned up after the test finishes.
+ *
+ * MAINTENANCE_TODO: Use this in more places. (Will be easier once .destroy() is allowed on
+ * invalid objects.)
+ */
+ trackForCleanup<T extends DestroyableObject>(o: T): T {
+ this.objectsToCleanUp.push(o);
+ return o;
+ }
+
+ /** Tracks an object, if it's destroyable, to be cleaned up after the test finishes. */
+ tryTrackForCleanup<T>(o: T): T {
+ if (typeof o === 'object' && o !== null) {
+ if (
+ 'destroy' in o ||
+ 'close' in o ||
+ o instanceof WebGLRenderingContext ||
+ o instanceof WebGL2RenderingContext
+ ) {
+ this.objectsToCleanUp.push((o as unknown) as DestroyableObject);
+ }
+ }
+ return o;
+ }
+
+ /** Log a debug message. */
+ debug(msg: string): void {
+ this.rec.debug(new Error(msg));
+ }
+
+ /** Throws an exception marking the subcase as skipped. */
+ skip(msg: string): never {
+ throw new SkipTestCase(msg);
+ }
+
+ /** Log a warning and increase the result status to "Warn". */
+ warn(msg?: string): void {
+ this.rec.warn(new Error(msg));
+ }
+
+ /** Log an error and increase the result status to "ExpectFailed". */
+ fail(msg?: string): void {
+ this.rec.expectationFailed(new Error(msg));
+ }
+
+ /**
+ * Wraps an async function. Tracks its status to fail if the test tries to report a test status
+ * before the async work has finished.
+ */
+ protected async immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> {
+ this.numOutstandingAsyncExpectations++;
+ const ret = await fn();
+ this.numOutstandingAsyncExpectations--;
+ return ret;
+ }
+
+ /**
+ * Wraps an async function, passing it an `Error` object recording the original stack trace.
+ * The async work will be implicitly waited upon before reporting a test status.
+ */
+ protected eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void {
+ const promise = fn(new Error());
+ this.eventualExpectations.push(promise);
+ }
+
+ private expectErrorValue(expectedError: string | true, ex: unknown, niceStack: Error): void {
+ if (!(ex instanceof Error)) {
+ niceStack.message = `THREW non-error value, of type ${typeof ex}: ${ex}`;
+ this.rec.expectationFailed(niceStack);
+ return;
+ }
+ const actualName = ex.name;
+ if (expectedError !== true && actualName !== expectedError) {
+ niceStack.message = `THREW ${actualName}, instead of ${expectedError}: ${ex}`;
+ this.rec.expectationFailed(niceStack);
+ } else {
+ niceStack.message = `OK: threw ${actualName}: ${ex.message}`;
+ this.rec.debug(niceStack);
+ }
+ }
+
+ /** Expect that the provided promise resolves (fulfills). */
+ shouldResolve(p: Promise<unknown>, msg?: string): void {
+ this.eventualAsyncExpectation(async niceStack => {
+ const m = msg ? ': ' + msg : '';
+ try {
+ await p;
+ niceStack.message = 'resolved as expected' + m;
+ } catch (ex) {
+ niceStack.message = `REJECTED${m}`;
+ if (ex instanceof Error) {
+ niceStack.message += '\n' + ex.message;
+ }
+ this.rec.expectationFailed(niceStack);
+ }
+ });
+ }
+
+ /** Expect that the provided promise rejects, with the provided exception name. */
+ shouldReject(expectedName: string, p: Promise<unknown>, msg?: string): void {
+ this.eventualAsyncExpectation(async niceStack => {
+ const m = msg ? ': ' + msg : '';
+ try {
+ await p;
+ niceStack.message = 'DID NOT REJECT' + m;
+ this.rec.expectationFailed(niceStack);
+ } catch (ex) {
+ niceStack.message = 'rejected as expected' + m;
+ this.expectErrorValue(expectedName, ex, niceStack);
+ }
+ });
+ }
+
+ /**
+ * Expect that the provided function throws.
+ * If an `expectedName` is provided, expect that the throw exception has that name.
+ */
+ shouldThrow(expectedError: string | boolean, fn: () => void, msg?: string): void {
+ const m = msg ? ': ' + msg : '';
+ try {
+ fn();
+ if (expectedError === false) {
+ this.rec.debug(new Error('did not throw, as expected' + m));
+ } else {
+ this.rec.expectationFailed(new Error('unexpectedly did not throw' + m));
+ }
+ } catch (ex) {
+ if (expectedError === false) {
+ this.rec.expectationFailed(new Error('threw unexpectedly' + m));
+ } else {
+ this.expectErrorValue(expectedError, ex, new Error(m));
+ }
+ }
+ }
+
+ /** Expect that a condition is true. */
+ expect(cond: boolean, msg?: string): boolean {
+ if (cond) {
+ const m = msg ? ': ' + msg : '';
+ this.rec.debug(new Error('expect OK' + m));
+ } else {
+ this.rec.expectationFailed(new Error(msg));
+ }
+ return cond;
+ }
+
+ /**
+ * If the argument is an `Error`, fail (or warn). If it's `undefined`, no-op.
+ * If the argument is an array, apply the above behavior on each of elements.
+ */
+ expectOK(
+ error: Error | undefined | (Error | undefined)[],
+ { mode = 'fail', niceStack }: { mode?: 'fail' | 'warn'; niceStack?: Error } = {}
+ ): void {
+ const handleError = (error: Error | undefined) => {
+ if (error instanceof Error) {
+ if (niceStack) {
+ error.stack = niceStack.stack;
+ }
+ if (mode === 'fail') {
+ this.rec.expectationFailed(error);
+ } else if (mode === 'warn') {
+ this.rec.warn(error);
+ } else {
+ unreachable();
+ }
+ }
+ };
+
+ if (Array.isArray(error)) {
+ for (const e of error) {
+ handleError(e);
+ }
+ } else {
+ handleError(error);
+ }
+ }
+
+ eventualExpectOK(
+ error: Promise<Error | undefined | (Error | undefined)[]>,
+ { mode = 'fail' }: { mode?: 'fail' | 'warn' } = {}
+ ) {
+ this.eventualAsyncExpectation(async niceStack => {
+ this.expectOK(await error, { mode, niceStack });
+ });
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts
new file mode 100644
index 0000000000..d22444a9b6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts
@@ -0,0 +1,337 @@
+import { Merged, mergeParams } from '../internal/params_utils.js';
+import { stringifyPublicParams } from '../internal/query/stringify_params.js';
+import { assert, mapLazy } from '../util/util.js';
+
+// ================================================================
+// "Public" ParamsBuilder API / Documentation
+// ================================================================
+
+/**
+ * Provides doc comments for the methods of CaseParamsBuilder and SubcaseParamsBuilder.
+ * (Also enforces rough interface match between them.)
+ */
+export interface ParamsBuilder {
+ /**
+ * Expands each item in `this` into zero or more items.
+ * Each item has its parameters expanded with those returned by the `expander`.
+ *
+ * **Note:** When only a single key is being added, use the simpler `expand` for readability.
+ *
+ * ```text
+ * this = [ a , b , c ]
+ * this.map(expander) = [ f(a) f(b) f(c) ]
+ * = [[a1, a2, a3] , [ b1 ] , [] ]
+ * merge and flatten = [ merge(a, a1), merge(a, a2), merge(a, a3), merge(b, b1) ]
+ * ```
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ expandWithParams(expander: (_: any) => any): any;
+
+ /**
+ * Expands each item in `this` into zero or more items. Each item has its parameters expanded
+ * with one new key, `key`, and the values returned by `expander`.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ expand(key: string, expander: (_: any) => any): any;
+
+ /**
+ * Expands each item in `this` to multiple items, one for each item in `newParams`.
+ *
+ * In other words, takes the cartesian product of [ the items in `this` ] and `newParams`.
+ *
+ * **Note:** When only a single key is being added, use the simpler `combine` for readability.
+ *
+ * ```text
+ * this = [ {a:1}, {b:2} ]
+ * newParams = [ {x:1}, {y:2} ]
+ * this.combineP(newParams) = [ {a:1,x:1}, {a:1,y:2}, {b:2,x:1}, {b:2,y:2} ]
+ * ```
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ combineWithParams(newParams: Iterable<any>): any;
+
+ /**
+ * Expands each item in `this` to multiple items with `{ [name]: value }` for each value.
+ *
+ * In other words, takes the cartesian product of [ the items in `this` ]
+ * and `[ {[name]: value} for each value in values ]`
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ combine(key: string, newParams: Iterable<any>): any;
+
+ /**
+ * Filters `this` to only items for which `pred` returns true.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ filter(pred: (_: any) => boolean): any;
+
+ /**
+ * Filters `this` to only items for which `pred` returns false.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ unless(pred: (_: any) => boolean): any;
+}
+
+/**
+ * Determines the resulting parameter object type which would be generated by an object of
+ * the given ParamsBuilder type.
+ */
+export type ParamTypeOf<
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ T extends ParamsBuilder
+> = T extends SubcaseParamsBuilder<infer CaseP, infer SubcaseP>
+ ? Merged<CaseP, SubcaseP>
+ : T extends CaseParamsBuilder<infer CaseP>
+ ? CaseP
+ : never;
+
+// ================================================================
+// Implementation
+// ================================================================
+
+/**
+ * Iterable over pairs of either:
+ * - `[case params, Iterable<subcase params>]` if there are subcases.
+ * - `[case params, undefined]` if not.
+ */
+export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
+ readonly [CaseP, Iterable<SubcaseP> | undefined]
+>;
+
+/**
+ * Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
+ */
+export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
+ protected readonly cases: () => Generator<CaseP>;
+
+ constructor(cases: () => Generator<CaseP>) {
+ this.cases = cases;
+ }
+
+ /**
+ * Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
+ */
+ protected abstract iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP>;
+}
+
+/**
+ * Calls the (normally hidden) `iterateCasesWithSubcases()` method.
+ */
+export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {}>) {
+ interface IterableParamsBuilder {
+ iterateCasesWithSubcases(): CaseSubcaseIterable<{}, {}>;
+ }
+
+ return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases();
+}
+
+/**
+ * Builder for combinatorial test **case** parameters.
+ *
+ * CaseParamsBuilder is immutable. Each method call returns a new, immutable object,
+ * modifying the list of cases according to the method called.
+ *
+ * This means, for example, that the `unit` passed into `TestBuilder.params()` can be reused.
+ */
+export class CaseParamsBuilder<CaseP extends {}>
+ extends ParamsBuilderBase<CaseP, {}>
+ implements Iterable<CaseP>, ParamsBuilder {
+ *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, {}> {
+ for (const a of this.cases()) {
+ yield [a, undefined];
+ }
+ }
+
+ [Symbol.iterator](): Iterator<CaseP> {
+ return this.cases();
+ }
+
+ /** @inheritDoc */
+ expandWithParams<NewP extends {}>(
+ expander: (_: Merged<{}, CaseP>) => Iterable<NewP>
+ ): CaseParamsBuilder<Merged<CaseP, NewP>> {
+ const newGenerator = expanderGenerator(this.cases, expander);
+ return new CaseParamsBuilder(() => newGenerator({}));
+ }
+
+ /** @inheritDoc */
+ expand<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ expander: (_: Merged<{}, CaseP>) => Iterable<NewPValue>
+ ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
+ return this.expandWithParams(function* (p) {
+ for (const value of expander(p)) {
+ yield { [key]: value } as { readonly [name in NewPKey]: NewPValue };
+ }
+ });
+ }
+
+ /** @inheritDoc */
+ combineWithParams<NewP extends {}>(
+ newParams: Iterable<NewP>
+ ): CaseParamsBuilder<Merged<CaseP, NewP>> {
+ assertNotGenerator(newParams);
+ const seenValues = new Set<string>();
+ for (const params of newParams) {
+ const paramsStr = stringifyPublicParams(params);
+ assert(!seenValues.has(paramsStr), `Duplicate entry in combine[WithParams]: ${paramsStr}`);
+ seenValues.add(paramsStr);
+ }
+
+ return this.expandWithParams(() => newParams);
+ }
+
+ /** @inheritDoc */
+ combine<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ values: Iterable<NewPValue>
+ ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
+ assertNotGenerator(values);
+ const mapped = mapLazy(values, v => ({ [key]: v } as { [name in NewPKey]: NewPValue }));
+ return this.combineWithParams(mapped);
+ }
+
+ /** @inheritDoc */
+ filter(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
+ const newGenerator = filterGenerator(this.cases, pred);
+ return new CaseParamsBuilder(() => newGenerator({}));
+ }
+
+ /** @inheritDoc */
+ unless(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
+ return this.filter(x => !pred(x));
+ }
+
+ /**
+ * "Finalize" the list of cases and begin defining subcases.
+ * Returns a new SubcaseParamsBuilder. Methods called on SubcaseParamsBuilder
+ * generate new subcases instead of new cases.
+ */
+ beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
+ return new SubcaseParamsBuilder(
+ () => this.cases(),
+ function* () {
+ yield {};
+ }
+ );
+ }
+}
+
+/**
+ * The unit CaseParamsBuilder, representing a single case with no params: `[ {} ]`.
+ *
+ * `punit` is passed to every `.params()`/`.paramsSubcasesOnly()` call, so `kUnitCaseParamsBuilder`
+ * is only explicitly needed if constructing a ParamsBuilder outside of a test builder.
+ */
+export const kUnitCaseParamsBuilder = new CaseParamsBuilder(function* () {
+ yield {};
+});
+
+/**
+ * Builder for combinatorial test _subcase_ parameters.
+ *
+ * SubcaseParamsBuilder is immutable. Each method call returns a new, immutable object,
+ * modifying the list of subcases according to the method called.
+ */
+export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
+ extends ParamsBuilderBase<CaseP, SubcaseP>
+ implements ParamsBuilder {
+ protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;
+
+ constructor(cases: () => Generator<CaseP>, generator: (_: CaseP) => Generator<SubcaseP>) {
+ super(cases);
+ this.subcases = generator;
+ }
+
+ *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP> {
+ for (const caseP of this.cases()) {
+ const subcases = Array.from(this.subcases(caseP));
+ if (subcases.length) {
+ yield [caseP, subcases];
+ }
+ }
+ }
+
+ /** @inheritDoc */
+ expandWithParams<NewP extends {}>(
+ expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
+ return new SubcaseParamsBuilder(this.cases, expanderGenerator(this.subcases, expander));
+ }
+
+ /** @inheritDoc */
+ expand<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
+ return this.expandWithParams(function* (p) {
+ for (const value of expander(p)) {
+ // TypeScript doesn't know here that NewPKey is always a single literal string type.
+ yield { [key]: value } as { [name in NewPKey]: NewPValue };
+ }
+ });
+ }
+
+ /** @inheritDoc */
+ combineWithParams<NewP extends {}>(
+ newParams: Iterable<NewP>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
+ assertNotGenerator(newParams);
+ return this.expandWithParams(() => newParams);
+ }
+
+ /** @inheritDoc */
+ combine<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ values: Iterable<NewPValue>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
+ assertNotGenerator(values);
+ return this.expand(key, () => values);
+ }
+
+ /** @inheritDoc */
+ filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
+ return new SubcaseParamsBuilder(this.cases, filterGenerator(this.subcases, pred));
+ }
+
+ /** @inheritDoc */
+ unless(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
+ return this.filter(x => !pred(x));
+ }
+}
+
+function expanderGenerator<Base, A, B>(
+ baseGenerator: (_: Base) => Generator<A>,
+ expander: (_: Merged<Base, A>) => Iterable<B>
+): (_: Base) => Generator<Merged<A, B>> {
+ return function* (base: Base) {
+ for (const a of baseGenerator(base)) {
+ for (const b of expander(mergeParams(base, a))) {
+ yield mergeParams(a, b);
+ }
+ }
+ };
+}
+
+function filterGenerator<Base, A>(
+ baseGenerator: (_: Base) => Generator<A>,
+ pred: (_: Merged<Base, A>) => boolean
+): (_: Base) => Generator<A> {
+ return function* (base: Base) {
+ for (const a of baseGenerator(base)) {
+ if (pred(mergeParams(base, a))) {
+ yield a;
+ }
+ }
+ };
+}
+
+/** Assert an object is not a Generator (a thing returned from a generator function). */
+function assertNotGenerator(x: object) {
+ if ('constructor' in x) {
+ assert(
+ x.constructor !== (function* () {})().constructor,
+ 'Argument must not be a generator, as generators are not reusable'
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts
new file mode 100644
index 0000000000..05451304b6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts
@@ -0,0 +1,110 @@
+/**
+ * Base path for resources. The default value is correct for non-worker WPT, but standalone and
+ * workers must access resources using a different base path, so this is overridden in
+ * `test_worker-worker.ts` and `standalone.ts`.
+ */
+let baseResourcePath = './resources';
+let crossOriginHost = '';
+
+function getAbsoluteBaseResourcePath(path: string) {
+ // Path is already an absolute one.
+ if (path[0] === '/') {
+ return path;
+ }
+
+ // Path is relative
+ const relparts = window.location.pathname.split('/');
+ relparts.pop();
+ const pathparts = path.split('/');
+
+ let i;
+ for (i = 0; i < pathparts.length; ++i) {
+ switch (pathparts[i]) {
+ case '':
+ break;
+ case '.':
+ break;
+ case '..':
+ relparts.pop();
+ break;
+ default:
+ relparts.push(pathparts[i]);
+ break;
+ }
+ }
+
+ return relparts.join('/');
+}
+
+function runningOnLocalHost(): boolean {
+ const hostname = window.location.hostname;
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
+}
+
+/**
+ * Get a path to a resource in the `resources` directory relative to the current execution context
+ * (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc but from cross origin host.
+ * Provide onlineUrl if the case running online.
+ * @internal MAINTENANCE_TODO: Cases may run in the LAN environment (not localhost but no internet
+ * access). We temporarily use `crossOriginHost` to configure the cross origin host name in that situation.
+ * But opening to auto-detect mechanism or other solutions.
+ */
+export function getCrossOriginResourcePath(pathRelativeToResourcesDir: string, onlineUrl = '') {
+ // A cross origin host has been configured. Use this to load resource.
+ if (crossOriginHost !== '') {
+ return (
+ crossOriginHost +
+ getAbsoluteBaseResourcePath(baseResourcePath) +
+ '/' +
+ pathRelativeToResourcesDir
+ );
+ }
+
+ // Using 'localhost' and '127.0.0.1' trick to load cross origin resource. Set cross origin host name
+ // to 'localhost' if case is not running in 'localhost' domain. Otherwise, use '127.0.0.1'.
+ // host name to locahost unless the server running in
+ if (runningOnLocalHost()) {
+ let crossOriginHostName = '';
+ if (location.hostname === 'localhost') {
+ crossOriginHostName = 'http://127.0.0.1';
+ } else {
+ crossOriginHostName = 'http://localhost';
+ }
+
+ return (
+ crossOriginHostName +
+ ':' +
+ location.port +
+ getAbsoluteBaseResourcePath(baseResourcePath) +
+ '/' +
+ pathRelativeToResourcesDir
+ );
+ }
+
+ return onlineUrl;
+}
+
+/**
+ * Get a path to a resource in the `resources` directory, relative to the current execution context
+ * (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc. Pass the cross origin host
+ * name if wants to load resoruce from cross origin host.
+ */
+export function getResourcePath(pathRelativeToResourcesDir: string) {
+ return baseResourcePath + '/' + pathRelativeToResourcesDir;
+}
+
+/**
+ * Set the base resource path (path to the `resources` directory relative to the current
+ * execution context).
+ */
+export function setBaseResourcePath(path: string) {
+ baseResourcePath = path;
+}
+
+/**
+ * Set the cross origin host and cases related to cross origin
+ * will load resource from the given host.
+ */
+export function setCrossOriginHost(host: string) {
+ crossOriginHost = host;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts
new file mode 100644
index 0000000000..bec74e20c5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts
@@ -0,0 +1,20 @@
+export type TestConfig = {
+ maxSubcasesInFlight: number;
+ testHeartbeatCallback: () => void;
+ noRaceWithRejectOnTimeout: boolean;
+
+ /**
+ * Controls the emission of loops in constant-evaluation shaders under
+ * 'webgpu:shader,execution,expression,*'
+ * FXC is extremely slow to compile shaders with loops unrolled, where as the
+ * MSL compiler is extremely slow to compile with loops rolled.
+ */
+ unrollConstEvalLoops: boolean;
+};
+
+export const globalTestConfig: TestConfig = {
+ maxSubcasesInFlight: 500,
+ testHeartbeatCallback: () => {},
+ noRaceWithRejectOnTimeout: false,
+ unrollConstEvalLoops: false,
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts
new file mode 100644
index 0000000000..5b761db9db
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts
@@ -0,0 +1 @@
+export { makeTestGroup } from '../internal/test_group.js';
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts
new file mode 100644
index 0000000000..922d6a09dd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts
@@ -0,0 +1,95 @@
+import { IterableTestGroup } from '../internal/test_group.js';
+import { assert } from '../util/util.js';
+
+import { parseQuery } from './query/parseQuery.js';
+import { TestQuery } from './query/query.js';
+import { TestSuiteListing } from './test_suite_listing.js';
+import { loadTreeForQuery, TestTree, TestTreeLeaf } from './tree.js';
+
+// A listing file, e.g. either of:
+// - `src/webgpu/listing.ts` (which is dynamically computed, has a Promise<TestSuiteListing>)
+// - `out/webgpu/listing.js` (which is pre-baked, has a TestSuiteListing)
+interface ListingFile {
+ listing: Promise<TestSuiteListing> | TestSuiteListing;
+}
+
+// A .spec.ts file, as imported.
+export interface SpecFile {
+ readonly description: string;
+ readonly g: IterableTestGroup;
+}
+
+export interface ImportInfo {
+ url: string;
+}
+
+interface TestFileLoaderEventMap {
+ import: MessageEvent<ImportInfo>;
+ finish: MessageEvent<void>;
+}
+
+export interface TestFileLoader extends EventTarget {
+ addEventListener<K extends keyof TestFileLoaderEventMap>(
+ type: K,
+ listener: (this: TestFileLoader, ev: TestFileLoaderEventMap[K]) => void,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ removeEventListener<K extends keyof TestFileLoaderEventMap>(
+ type: K,
+ listener: (this: TestFileLoader, ev: TestFileLoaderEventMap[K]) => void,
+ options?: boolean | EventListenerOptions
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions
+ ): void;
+}
+
+// Base class for DefaultTestFileLoader and FakeTestFileLoader.
+export abstract class TestFileLoader extends EventTarget {
+ abstract listing(suite: string): Promise<TestSuiteListing>;
+ protected abstract import(path: string): Promise<SpecFile>;
+
+ importSpecFile(suite: string, path: string[]): Promise<SpecFile> {
+ const url = `${suite}/${path.join('/')}.spec.js`;
+ this.dispatchEvent(
+ new MessageEvent<ImportInfo>('import', { data: { url } })
+ );
+ return this.import(url);
+ }
+
+ async loadTree(query: TestQuery, subqueriesToExpand: string[] = []): Promise<TestTree> {
+ const tree = await loadTreeForQuery(
+ this,
+ query,
+ subqueriesToExpand.map(s => {
+ const q = parseQuery(s);
+ assert(q.level >= 2, () => `subqueriesToExpand entries should not be multi-file:\n ${q}`);
+ return q;
+ })
+ );
+ this.dispatchEvent(new MessageEvent<void>('finish'));
+ return tree;
+ }
+
+ async loadCases(query: TestQuery): Promise<IterableIterator<TestTreeLeaf>> {
+ const tree = await this.loadTree(query);
+ return tree.iterateLeaves();
+ }
+}
+
+export class DefaultTestFileLoader extends TestFileLoader {
+ async listing(suite: string): Promise<TestSuiteListing> {
+ return ((await import(`../../${suite}/listing.js`)) as ListingFile).listing;
+ }
+
+ import(path: string): Promise<SpecFile> {
+ return import(`../../${path}`);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts
new file mode 100644
index 0000000000..ee006cdeb3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts
@@ -0,0 +1,44 @@
+import { ErrorWithExtra } from '../../util/util.js';
+import { extractImportantStackTrace } from '../stack.js';
+
+export class LogMessageWithStack extends Error {
+ readonly extra: unknown;
+
+ private stackHiddenMessage: string | undefined = undefined;
+
+ constructor(name: string, ex: Error | ErrorWithExtra) {
+ super(ex.message);
+
+ this.name = name;
+ this.stack = ex.stack;
+ if ('extra' in ex) {
+ this.extra = ex.extra;
+ }
+ }
+
+ /** Set a flag so the stack is not printed in toJSON(). */
+ setStackHidden(stackHiddenMessage: string) {
+ this.stackHiddenMessage ??= stackHiddenMessage;
+ }
+
+ toJSON(): string {
+ let m = this.name;
+ if (this.message) m += ': ' + this.message;
+ if (this.stack) {
+ if (this.stackHiddenMessage === undefined) {
+ m += '\n' + extractImportantStackTrace(this);
+ } else if (this.stackHiddenMessage) {
+ m += `\n at (elided: ${this.stackHiddenMessage})`;
+ }
+ }
+ return m;
+ }
+}
+
+/**
+ * Returns a string, nicely indented, for debug logs.
+ * This is used in the cmdline and wpt runtimes. In WPT, it shows up in the `*-actual.txt` file.
+ */
+export function prettyPrintLog(log: LogMessageWithStack): string {
+ return ' - ' + log.toJSON().replace(/\n/g, '\n ');
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts
new file mode 100644
index 0000000000..e4526cff54
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts
@@ -0,0 +1,30 @@
+import { version } from '../version.js';
+
+import { LiveTestCaseResult } from './result.js';
+import { TestCaseRecorder } from './test_case_recorder.js';
+
+export type LogResults = Map<string, LiveTestCaseResult>;
+
+export class Logger {
+ static globalDebugMode: boolean = false;
+
+ readonly overriddenDebugMode: boolean | undefined;
+ readonly results: LogResults = new Map();
+
+ constructor({ overrideDebugMode }: { overrideDebugMode?: boolean } = {}) {
+ this.overriddenDebugMode = overrideDebugMode;
+ }
+
+ record(name: string): [TestCaseRecorder, LiveTestCaseResult] {
+ const result: LiveTestCaseResult = { status: 'running', timems: -1 };
+ this.results.set(name, result);
+ return [
+ new TestCaseRecorder(result, this.overriddenDebugMode ?? Logger.globalDebugMode),
+ result,
+ ];
+ }
+
+ asJSON(space?: number): string {
+ return JSON.stringify({ version, results: Array.from(this.results) }, undefined, space);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts
new file mode 100644
index 0000000000..0de661b50c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts
@@ -0,0 +1,21 @@
+import { LogMessageWithStack } from './log_message.js';
+
+// MAINTENANCE_TODO: Add warn expectations
+export type Expectation = 'pass' | 'skip' | 'fail';
+
+export type Status = 'running' | 'warn' | Expectation;
+
+export interface TestCaseResult {
+ status: Status;
+ timems: number;
+}
+
+export interface LiveTestCaseResult extends TestCaseResult {
+ logs?: LogMessageWithStack[];
+}
+
+export interface TransferredTestCaseResult extends TestCaseResult {
+ // When transferred from a worker, a LogMessageWithStack turns into a generic Error
+ // (its prototype gets lost and replaced with Error).
+ logs?: Error[];
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts
new file mode 100644
index 0000000000..7507bbdec6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts
@@ -0,0 +1,158 @@
+import { SkipTestCase, UnexpectedPassError } from '../../framework/fixture.js';
+import { globalTestConfig } from '../../framework/test_config.js';
+import { now, assert } from '../../util/util.js';
+
+import { LogMessageWithStack } from './log_message.js';
+import { Expectation, LiveTestCaseResult } from './result.js';
+
+enum LogSeverity {
+ Pass = 0,
+ Skip = 1,
+ Warn = 2,
+ ExpectFailed = 3,
+ ValidationFailed = 4,
+ ThrewException = 5,
+}
+
+const kMaxLogStacks = 2;
+const kMinSeverityForStack = LogSeverity.Warn;
+
+/** Holds onto a LiveTestCaseResult owned by the Logger, and writes the results into it. */
+export class TestCaseRecorder {
+ private result: LiveTestCaseResult;
+ private inSubCase: boolean = false;
+ private subCaseStatus = LogSeverity.Pass;
+ private finalCaseStatus = LogSeverity.Pass;
+ private hideStacksBelowSeverity = kMinSeverityForStack;
+ private startTime = -1;
+ private logs: LogMessageWithStack[] = [];
+ private logLinesAtCurrentSeverity = 0;
+ private debugging = false;
+ /** Used to dedup log messages which have identical stacks. */
+ private messagesForPreviouslySeenStacks = new Map<string, LogMessageWithStack>();
+
+ constructor(result: LiveTestCaseResult, debugging: boolean) {
+ this.result = result;
+ this.debugging = debugging;
+ }
+
+ start(): void {
+ assert(this.startTime < 0, 'TestCaseRecorder cannot be reused');
+ this.startTime = now();
+ }
+
+ finish(): void {
+ assert(this.startTime >= 0, 'finish() before start()');
+
+ const timeMilliseconds = now() - this.startTime;
+ // Round to next microsecond to avoid storing useless .xxxx00000000000002 in results.
+ this.result.timems = Math.ceil(timeMilliseconds * 1000) / 1000;
+
+ // Convert numeric enum back to string (but expose 'exception' as 'fail')
+ this.result.status =
+ this.finalCaseStatus === LogSeverity.Pass
+ ? 'pass'
+ : this.finalCaseStatus === LogSeverity.Skip
+ ? 'skip'
+ : this.finalCaseStatus === LogSeverity.Warn
+ ? 'warn'
+ : 'fail'; // Everything else is an error
+
+ this.result.logs = this.logs;
+ }
+
+ beginSubCase() {
+ this.subCaseStatus = LogSeverity.Pass;
+ this.inSubCase = true;
+ }
+
+ endSubCase(expectedStatus: Expectation) {
+ try {
+ if (expectedStatus === 'fail') {
+ if (this.subCaseStatus <= LogSeverity.Warn) {
+ throw new UnexpectedPassError();
+ } else {
+ this.subCaseStatus = LogSeverity.Pass;
+ }
+ }
+ } finally {
+ this.inSubCase = false;
+ if (this.subCaseStatus > this.finalCaseStatus) {
+ this.finalCaseStatus = this.subCaseStatus;
+ }
+ }
+ }
+
+ injectResult(injectedResult: LiveTestCaseResult): void {
+ Object.assign(this.result, injectedResult);
+ }
+
+ debug(ex: Error): void {
+ if (!this.debugging) return;
+ this.logImpl(LogSeverity.Pass, 'DEBUG', ex);
+ }
+
+ info(ex: Error): void {
+ this.logImpl(LogSeverity.Pass, 'INFO', ex);
+ }
+
+ skipped(ex: SkipTestCase): void {
+ this.logImpl(LogSeverity.Skip, 'SKIP', ex);
+ }
+
+ warn(ex: Error): void {
+ this.logImpl(LogSeverity.Warn, 'WARN', ex);
+ }
+
+ expectationFailed(ex: Error): void {
+ this.logImpl(LogSeverity.ExpectFailed, 'EXPECTATION FAILED', ex);
+ }
+
+ validationFailed(ex: Error): void {
+ this.logImpl(LogSeverity.ValidationFailed, 'VALIDATION FAILED', ex);
+ }
+
+ threw(ex: unknown): void {
+ if (ex instanceof SkipTestCase) {
+ this.skipped(ex);
+ return;
+ }
+ this.logImpl(LogSeverity.ThrewException, 'EXCEPTION', ex);
+ }
+
+ private logImpl(level: LogSeverity, name: string, baseException: unknown): void {
+ assert(baseException instanceof Error, 'test threw a non-Error object');
+ globalTestConfig.testHeartbeatCallback();
+ const logMessage = new LogMessageWithStack(name, baseException);
+
+ // Final case status should be the "worst" of all log entries.
+ if (this.inSubCase) {
+ if (level > this.subCaseStatus) this.subCaseStatus = level;
+ } else {
+ if (level > this.finalCaseStatus) this.finalCaseStatus = level;
+ }
+
+ // setFirstLineOnly for all logs except `kMaxLogStacks` stacks at the highest severity
+ if (level > this.hideStacksBelowSeverity) {
+ this.logLinesAtCurrentSeverity = 0;
+ this.hideStacksBelowSeverity = level;
+
+ // Go back and setFirstLineOnly for everything of a lower log level
+ for (const log of this.logs) {
+ log.setStackHidden('below max severity');
+ }
+ }
+ if (level === this.hideStacksBelowSeverity) {
+ this.logLinesAtCurrentSeverity++;
+ } else if (level < kMinSeverityForStack) {
+ logMessage.setStackHidden('');
+ } else if (level < this.hideStacksBelowSeverity) {
+ logMessage.setStackHidden('below max severity');
+ }
+ if (this.logLinesAtCurrentSeverity > kMaxLogStacks) {
+ logMessage.setStackHidden(`only ${kMaxLogStacks} shown`);
+ }
+
+ this.logs.push(logMessage);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/params_utils.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/params_utils.ts
new file mode 100644
index 0000000000..07d2f836f1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/params_utils.ts
@@ -0,0 +1,124 @@
+import { TestParams } from '../framework/fixture.js';
+import { ResolveType, UnionToIntersection } from '../util/types.js';
+import { assert } from '../util/util.js';
+
+import { comparePublicParamsPaths, Ordering } from './query/compare.js';
+import { kWildcard, kParamSeparator, kParamKVSeparator } from './query/separators.js';
+
+export type JSONWithUndefined =
+ | undefined
+ | null
+ | number
+ | string
+ | boolean
+ | readonly JSONWithUndefined[]
+ // Ideally this would recurse into JSONWithUndefined, but it breaks code.
+ | { readonly [k: string]: unknown };
+export interface TestParamsRW {
+ [k: string]: JSONWithUndefined;
+}
+export type TestParamsIterable = Iterable<TestParams>;
+
+export function paramKeyIsPublic(key: string): boolean {
+ return !key.startsWith('_');
+}
+
+export function extractPublicParams(params: TestParams): TestParams {
+ const publicParams: TestParamsRW = {};
+ for (const k of Object.keys(params)) {
+ if (paramKeyIsPublic(k)) {
+ publicParams[k] = params[k];
+ }
+ }
+ return publicParams;
+}
+
+export const badParamValueChars = new RegExp(
+ '[' + kParamKVSeparator + kParamSeparator + kWildcard + ']'
+);
+
+export function publicParamsEquals(x: TestParams, y: TestParams): boolean {
+ return comparePublicParamsPaths(x, y) === Ordering.Equal;
+}
+
+export type KeyOfNeverable<T> = T extends never ? never : keyof T;
+export type AllKeysFromUnion<T> = keyof T | KeyOfNeverable<UnionToIntersection<T>>;
+export type KeyOfOr<T, K, Default> = K extends keyof T ? T[K] : Default;
+
+/**
+ * Flatten a union of interfaces into a single interface encoding the same type.
+ *
+ * Flattens a union in such a way that:
+ * `{ a: number, b?: undefined } | { b: string, a?: undefined }`
+ * (which is the value type of `[{ a: 1 }, { b: 1 }]`)
+ * becomes `{ a: number | undefined, b: string | undefined }`.
+ *
+ * And also works for `{ a: number } | { b: string }` which maps to the same.
+ */
+export type FlattenUnionOfInterfaces<T> = {
+ [K in AllKeysFromUnion<T>]: KeyOfOr<
+ T,
+ // If T always has K, just take T[K] (union of C[K] for each component C of T):
+ K,
+ // Otherwise, take the union of C[K] for each component C of T, PLUS undefined:
+ undefined | KeyOfOr<UnionToIntersection<T>, K, void>
+ >;
+};
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function typeAssert<T extends 'pass'>() {}
+{
+ type Test<T, U> = [T] extends [U]
+ ? [U] extends [T]
+ ? 'pass'
+ : { actual: ResolveType<T>; expected: U }
+ : { actual: ResolveType<T>; expected: U };
+
+ type T01 = { a: number } | { b: string };
+ type T02 = { a: number } | { b?: string };
+ type T03 = { a: number } | { a?: number };
+ type T04 = { a: number } | { a: string };
+ type T05 = { a: number } | { a?: string };
+
+ type T11 = { a: number; b?: undefined } | { a?: undefined; b: string };
+
+ type T21 = { a: number; b?: undefined } | { b: string };
+ type T22 = { a: number; b?: undefined } | { b?: string };
+ type T23 = { a: number; b?: undefined } | { a?: number };
+ type T24 = { a: number; b?: undefined } | { a: string };
+ type T25 = { a: number; b?: undefined } | { a?: string };
+ type T26 = { a: number; b?: undefined } | { a: undefined };
+ type T27 = { a: number; b?: undefined } | { a: undefined; b: undefined };
+
+ /* prettier-ignore */ {
+ typeAssert<Test<FlattenUnionOfInterfaces<T01>, { a: number | undefined; b: string | undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T02>, { a: number | undefined; b: string | undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T03>, { a: number | undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T04>, { a: number | string }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T05>, { a: number | string | undefined }>>();
+
+ typeAssert<Test<FlattenUnionOfInterfaces<T11>, { a: number | undefined; b: string | undefined }>>();
+
+ typeAssert<Test<FlattenUnionOfInterfaces<T22>, { a: number | undefined; b: string | undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T23>, { a: number | undefined; b: undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T24>, { a: number | string; b: undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T25>, { a: number | string | undefined; b: undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T27>, { a: number | undefined; b: undefined }>>();
+
+ // Unexpected test results - hopefully okay to ignore these
+ typeAssert<Test<FlattenUnionOfInterfaces<T21>, { b: string | undefined }>>();
+ typeAssert<Test<FlattenUnionOfInterfaces<T26>, { a: number | undefined }>>();
+ }
+}
+
+export type Merged<A, B> = MergedFromFlat<A, FlattenUnionOfInterfaces<B>>;
+export type MergedFromFlat<A, B> = {
+ [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
+};
+
+export function mergeParams<A extends {}, B extends {}>(a: A, b: B): Merged<A, B> {
+ for (const key of Object.keys(a)) {
+ assert(!(key in b), 'Duplicate key: ' + key);
+ }
+ return { ...a, ...b } as Merged<A, B>;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts
new file mode 100644
index 0000000000..e9f4b01503
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts
@@ -0,0 +1,94 @@
+import { TestParams } from '../../framework/fixture.js';
+import { assert, objectEquals } from '../../util/util.js';
+import { paramKeyIsPublic } from '../params_utils.js';
+
+import { TestQuery } from './query.js';
+
+export const enum Ordering {
+ Unordered,
+ StrictSuperset,
+ Equal,
+ StrictSubset,
+}
+
+/**
+ * Compares two queries for their ordering (which is used to build the tree).
+ *
+ * See src/unittests/query_compare.spec.ts for examples.
+ */
+export function compareQueries(a: TestQuery, b: TestQuery): Ordering {
+ if (a.suite !== b.suite) {
+ return Ordering.Unordered;
+ }
+
+ const filePathOrdering = comparePaths(a.filePathParts, b.filePathParts);
+ if (filePathOrdering !== Ordering.Equal || a.isMultiFile || b.isMultiFile) {
+ return compareOneLevel(filePathOrdering, a.isMultiFile, b.isMultiFile);
+ }
+ assert('testPathParts' in a && 'testPathParts' in b);
+
+ const testPathOrdering = comparePaths(a.testPathParts, b.testPathParts);
+ if (testPathOrdering !== Ordering.Equal || a.isMultiTest || b.isMultiTest) {
+ return compareOneLevel(testPathOrdering, a.isMultiTest, b.isMultiTest);
+ }
+ assert('params' in a && 'params' in b);
+
+ const paramsPathOrdering = comparePublicParamsPaths(a.params, b.params);
+ if (paramsPathOrdering !== Ordering.Equal || a.isMultiCase || b.isMultiCase) {
+ return compareOneLevel(paramsPathOrdering, a.isMultiCase, b.isMultiCase);
+ }
+ return Ordering.Equal;
+}
+
+/**
+ * Compares a single level of a query.
+ *
+ * "IsBig" means the query is big relative to the level, e.g. for test-level:
+ * - Anything >= `suite:a,*` is big
+ * - Anything <= `suite:a:*` is small
+ */
+function compareOneLevel(ordering: Ordering, aIsBig: boolean, bIsBig: boolean): Ordering {
+ assert(ordering !== Ordering.Equal || aIsBig || bIsBig);
+ if (ordering === Ordering.Unordered) return Ordering.Unordered;
+ if (aIsBig && bIsBig) return ordering;
+ if (!aIsBig && !bIsBig) return Ordering.Unordered; // Equal case is already handled
+ // Exactly one of (a, b) is big.
+ if (aIsBig && ordering !== Ordering.StrictSubset) return Ordering.StrictSuperset;
+ if (bIsBig && ordering !== Ordering.StrictSuperset) return Ordering.StrictSubset;
+ return Ordering.Unordered;
+}
+
+function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
+ const shorter = Math.min(a.length, b.length);
+
+ for (let i = 0; i < shorter; ++i) {
+ if (a[i] !== b[i]) {
+ return Ordering.Unordered;
+ }
+ }
+ if (a.length === b.length) {
+ return Ordering.Equal;
+ } else if (a.length < b.length) {
+ return Ordering.StrictSuperset;
+ } else {
+ return Ordering.StrictSubset;
+ }
+}
+
+export function comparePublicParamsPaths(a: TestParams, b: TestParams): Ordering {
+ const aKeys = Object.keys(a).filter(k => paramKeyIsPublic(k));
+ const commonKeys = new Set(aKeys.filter(k => k in b));
+
+ for (const k of commonKeys) {
+ if (!objectEquals(a[k], b[k])) {
+ return Ordering.Unordered;
+ }
+ }
+ const bKeys = Object.keys(b).filter(k => paramKeyIsPublic(k));
+ const aRemainingKeys = aKeys.length - commonKeys.size;
+ const bRemainingKeys = bKeys.length - commonKeys.size;
+ if (aRemainingKeys === 0 && bRemainingKeys === 0) return Ordering.Equal;
+ if (aRemainingKeys === 0) return Ordering.StrictSuperset;
+ if (bRemainingKeys === 0) return Ordering.StrictSubset;
+ return Ordering.Unordered;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/encode_selectively.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/encode_selectively.ts
new file mode 100644
index 0000000000..ab1997b6e4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/encode_selectively.ts
@@ -0,0 +1,23 @@
+/**
+ * Encodes a stringified TestQuery so that it can be placed in a `?q=` parameter in a URL.
+ *
+ * `encodeURIComponent` encodes in accordance with `application/x-www-form-urlencoded`,
+ * but URLs don't actually have to be as strict as HTML form encoding
+ * (we interpret this purely from JavaScript).
+ * So we encode the component, then selectively convert some %-encoded escape codes
+ * back to their original form for readability/copyability.
+ */
+export function encodeURIComponentSelectively(s: string): string {
+ let ret = encodeURIComponent(s);
+ ret = ret.replace(/%22/g, '"'); // for JSON strings
+ ret = ret.replace(/%2C/g, ','); // for path separator, and JSON arrays
+ ret = ret.replace(/%3A/g, ':'); // for big separator
+ ret = ret.replace(/%3B/g, ';'); // for param separator
+ ret = ret.replace(/%3D/g, '='); // for params (k=v)
+ ret = ret.replace(/%5B/g, '['); // for JSON arrays
+ ret = ret.replace(/%5D/g, ']'); // for JSON arrays
+ ret = ret.replace(/%7B/g, '{'); // for JSON objects
+ ret = ret.replace(/%7D/g, '}'); // for JSON objects
+ ret = ret.replace(/%E2%9C%97/g, '✗'); // for jsUndefinedMagicValue
+ return ret;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/json_param_value.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/json_param_value.ts
new file mode 100644
index 0000000000..f4be7642d3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/json_param_value.ts
@@ -0,0 +1,83 @@
+import { assert, sortObjectByKey } from '../../util/util.js';
+import { JSONWithUndefined } from '../params_utils.js';
+
+// JSON can't represent various values and by default stores them as `null`.
+// Instead, storing them as a magic string values in JSON.
+const jsUndefinedMagicValue = '_undef_';
+const jsNaNMagicValue = '_nan_';
+const jsPositiveInfinityMagicValue = '_posinfinity_';
+const jsNegativeInfinityMagicValue = '_neginfinity_';
+
+// -0 needs to be handled separately, because -0 === +0 returns true. Not
+// special casing +0/0, since it behaves intuitively. Assuming that if -0 is
+// being used, the differentiation from +0 is desired.
+const jsNegativeZeroMagicValue = '_negzero_';
+
+const toStringMagicValue = new Map<unknown, string>([
+ [undefined, jsUndefinedMagicValue],
+ [NaN, jsNaNMagicValue],
+ [Number.POSITIVE_INFINITY, jsPositiveInfinityMagicValue],
+ [Number.NEGATIVE_INFINITY, jsNegativeInfinityMagicValue],
+ // No -0 handling because it is special cased.
+]);
+
+const fromStringMagicValue = new Map<string, unknown>([
+ [jsUndefinedMagicValue, undefined],
+ [jsNaNMagicValue, NaN],
+ [jsPositiveInfinityMagicValue, Number.POSITIVE_INFINITY],
+ [jsNegativeInfinityMagicValue, Number.NEGATIVE_INFINITY],
+ // -0 is handled in this direction because there is no comparison issue.
+ [jsNegativeZeroMagicValue, -0],
+]);
+
+function stringifyFilter(k: string, v: unknown): unknown {
+ // Make sure no one actually uses a magic value as a parameter.
+ if (typeof v === 'string') {
+ assert(
+ !fromStringMagicValue.has(v),
+ `${v} is a magic value for stringification, so cannot be used`
+ );
+
+ assert(
+ v !== jsNegativeZeroMagicValue,
+ `${v} is a magic value for stringification, so cannot be used`
+ );
+ }
+
+ if (Object.is(v, -0)) {
+ return jsNegativeZeroMagicValue;
+ }
+
+ return toStringMagicValue.has(v) ? toStringMagicValue.get(v) : v;
+}
+
+export function stringifyParamValue(value: JSONWithUndefined): string {
+ return JSON.stringify(value, stringifyFilter);
+}
+
+/**
+ * Like stringifyParamValue but sorts dictionaries by key, for hashing.
+ */
+export function stringifyParamValueUniquely(value: JSONWithUndefined): string {
+ return JSON.stringify(value, (k, v) => {
+ if (typeof v === 'object' && v !== null) {
+ return sortObjectByKey(v);
+ }
+
+ return stringifyFilter(k, v);
+ });
+}
+
+// 'any' is part of the JSON.parse reviver interface, so cannot be avoided.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function parseParamValueReviver(k: string, v: any): any {
+ if (fromStringMagicValue.has(v)) {
+ return fromStringMagicValue.get(v);
+ }
+
+ return v;
+}
+
+export function parseParamValue(s: string): JSONWithUndefined {
+ return JSON.parse(s, parseParamValueReviver);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts
new file mode 100644
index 0000000000..996835b0ec
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts
@@ -0,0 +1,155 @@
+import { assert } from '../../util/util.js';
+import {
+ TestParamsRW,
+ JSONWithUndefined,
+ badParamValueChars,
+ paramKeyIsPublic,
+} from '../params_utils.js';
+
+import { parseParamValue } from './json_param_value.js';
+import {
+ TestQuery,
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+ TestQueryMultiCase,
+ TestQuerySingleCase,
+} from './query.js';
+import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js';
+import { validQueryPart } from './validQueryPart.js';
+
+export function parseQuery(s: string): TestQuery {
+ try {
+ return parseQueryImpl(s);
+ } catch (ex) {
+ if (ex instanceof Error) {
+ ex.message += '\n on: ' + s;
+ }
+ throw ex;
+ }
+}
+
+function parseQueryImpl(s: string): TestQuery {
+ // Undo encodeURIComponentSelectively
+ s = decodeURIComponent(s);
+
+ // bigParts are: suite, file, test, params (note kBigSeparator could appear in params)
+ let suite: string;
+ let fileString: string | undefined;
+ let testString: string | undefined;
+ let paramsString: string | undefined;
+ {
+ const i1 = s.indexOf(kBigSeparator);
+ assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`);
+ suite = s.substring(0, i1);
+ const i2 = s.indexOf(kBigSeparator, i1 + 1);
+ if (i2 === -1) {
+ fileString = s.substring(i1 + 1);
+ } else {
+ fileString = s.substring(i1 + 1, i2);
+ const i3 = s.indexOf(kBigSeparator, i2 + 1);
+ if (i3 === -1) {
+ testString = s.substring(i2 + 1);
+ } else {
+ testString = s.substring(i2 + 1, i3);
+ paramsString = s.substring(i3 + 1);
+ }
+ }
+ }
+
+ const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator);
+
+ if (testString === undefined) {
+ // Query is file-level
+ assert(
+ filePathHasWildcard,
+ `File-level query without wildcard ${kWildcard}. Did you want a file-level query \
+(append ${kPathSeparator}${kWildcard}) or test-level query (append ${kBigSeparator}${kWildcard})?`
+ );
+ return new TestQueryMultiFile(suite, file);
+ }
+ assert(!filePathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
+
+ const { parts: test, wildcard: testPathHasWildcard } = parseBigPart(testString, kPathSeparator);
+
+ if (paramsString === undefined) {
+ // Query is test-level
+ assert(
+ testPathHasWildcard,
+ `Test-level query without wildcard ${kWildcard}; did you want a test-level query \
+(append ${kPathSeparator}${kWildcard}) or case-level query (append ${kBigSeparator}${kWildcard})?`
+ );
+ assert(file.length > 0, 'File part of test-level query was empty (::)');
+ return new TestQueryMultiTest(suite, file, test);
+ }
+
+ // Query is case-level
+ assert(!testPathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
+
+ const { parts: paramsParts, wildcard: paramsHasWildcard } = parseBigPart(
+ paramsString,
+ kParamSeparator
+ );
+
+ assert(test.length > 0, 'Test part of case-level query was empty (::)');
+
+ const params: TestParamsRW = {};
+ for (const paramPart of paramsParts) {
+ const [k, v] = parseSingleParam(paramPart);
+ assert(validQueryPart.test(k), `param key names must match ${validQueryPart}`);
+ params[k] = v;
+ }
+ if (paramsHasWildcard) {
+ return new TestQueryMultiCase(suite, file, test, params);
+ } else {
+ return new TestQuerySingleCase(suite, file, test, params);
+ }
+}
+
+// webgpu:a,b,* or webgpu:a,b,c:*
+const kExampleQueries = `\
+webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}${kWildcard} or \
+webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}c${kBigSeparator}${kWildcard}`;
+
+function parseBigPart(
+ s: string,
+ separator: typeof kParamSeparator | typeof kPathSeparator
+): { parts: string[]; wildcard: boolean } {
+ if (s === '') {
+ return { parts: [], wildcard: false };
+ }
+ const parts = s.split(separator);
+
+ let endsWithWildcard = false;
+ for (const [i, part] of parts.entries()) {
+ if (i === parts.length - 1) {
+ endsWithWildcard = part === kWildcard;
+ }
+ assert(
+ part.indexOf(kWildcard) === -1 || endsWithWildcard,
+ `Wildcard ${kWildcard} must be complete last part of a path (e.g. ${kExampleQueries})`
+ );
+ }
+ if (endsWithWildcard) {
+ // Remove the last element of the array (which is just the wildcard).
+ parts.length = parts.length - 1;
+ }
+ return { parts, wildcard: endsWithWildcard };
+}
+
+function parseSingleParam(paramSubstring: string): [string, JSONWithUndefined] {
+ assert(paramSubstring !== '', 'Param in a query must not be blank (is there a trailing comma?)');
+ const i = paramSubstring.indexOf('=');
+ assert(i !== -1, 'Param in a query must be of form key=value');
+ const k = paramSubstring.substring(0, i);
+ assert(paramKeyIsPublic(k), 'Param in a query must not be private (start with _)');
+ const v = paramSubstring.substring(i + 1);
+ return [k, parseSingleParamValue(v)];
+}
+
+function parseSingleParamValue(s: string): JSONWithUndefined {
+ assert(
+ !badParamValueChars.test(s),
+ `param value must not match ${badParamValueChars} - was ${s}`
+ );
+ return parseParamValue(s);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts
new file mode 100644
index 0000000000..59e96cb538
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts
@@ -0,0 +1,262 @@
+import { TestParams } from '../../framework/fixture.js';
+import { optionEnabled } from '../../runtime/helper/options.js';
+import { assert, unreachable } from '../../util/util.js';
+import { Expectation } from '../logging/result.js';
+
+import { compareQueries, Ordering } from './compare.js';
+import { encodeURIComponentSelectively } from './encode_selectively.js';
+import { parseQuery } from './parseQuery.js';
+import { kBigSeparator, kPathSeparator, kWildcard } from './separators.js';
+import { stringifyPublicParams } from './stringify_params.js';
+
+/**
+ * Represents a test query of some level.
+ *
+ * TestQuery types are immutable.
+ */
+export type TestQuery =
+ | TestQuerySingleCase
+ | TestQueryMultiCase
+ | TestQueryMultiTest
+ | TestQueryMultiFile;
+
+/**
+ * - 1 = MultiFile.
+ * - 2 = MultiTest.
+ * - 3 = MultiCase.
+ * - 4 = SingleCase.
+ */
+export type TestQueryLevel = 1 | 2 | 3 | 4;
+
+export interface TestQueryWithExpectation {
+ query: TestQuery;
+ expectation: Expectation;
+}
+
+/**
+ * A multi-file test query, like `s:*` or `s:a,b,*`.
+ *
+ * Immutable (makes copies of constructor args).
+ */
+export class TestQueryMultiFile {
+ readonly level: TestQueryLevel = 1;
+ readonly isMultiFile: boolean = true;
+ readonly suite: string;
+ readonly filePathParts: readonly string[];
+
+ constructor(suite: string, file: readonly string[]) {
+ this.suite = suite;
+ this.filePathParts = [...file];
+ }
+
+ get depthInLevel() {
+ return this.filePathParts.length;
+ }
+
+ toString(): string {
+ return encodeURIComponentSelectively(this.toStringHelper().join(kBigSeparator));
+ }
+
+ protected toStringHelper(): string[] {
+ return [this.suite, [...this.filePathParts, kWildcard].join(kPathSeparator)];
+ }
+}
+
+/**
+ * A multi-test test query, like `s:f:*` or `s:f:a,b,*`.
+ *
+ * Immutable (makes copies of constructor args).
+ */
+export class TestQueryMultiTest extends TestQueryMultiFile {
+ readonly level: TestQueryLevel = 2;
+ readonly isMultiFile: false = false;
+ readonly isMultiTest: boolean = true;
+ readonly testPathParts: readonly string[];
+
+ constructor(suite: string, file: readonly string[], test: readonly string[]) {
+ super(suite, file);
+ assert(file.length > 0, 'multi-test (or finer) query must have file-path');
+ this.testPathParts = [...test];
+ }
+
+ get depthInLevel() {
+ return this.testPathParts.length;
+ }
+
+ protected toStringHelper(): string[] {
+ return [
+ this.suite,
+ this.filePathParts.join(kPathSeparator),
+ [...this.testPathParts, kWildcard].join(kPathSeparator),
+ ];
+ }
+}
+
+/**
+ * A multi-case test query, like `s:f:t:*` or `s:f:t:a,b,*`.
+ *
+ * Immutable (makes copies of constructor args), except for param values
+ * (which aren't normally supposed to change; they're marked readonly in TestParams).
+ */
+export class TestQueryMultiCase extends TestQueryMultiTest {
+ readonly level: TestQueryLevel = 3;
+ readonly isMultiTest: false = false;
+ readonly isMultiCase: boolean = true;
+ readonly params: TestParams;
+
+ constructor(suite: string, file: readonly string[], test: readonly string[], params: TestParams) {
+ super(suite, file, test);
+ assert(test.length > 0, 'multi-case (or finer) query must have test-path');
+ this.params = { ...params };
+ }
+
+ get depthInLevel() {
+ return Object.keys(this.params).length;
+ }
+
+ protected toStringHelper(): string[] {
+ return [
+ this.suite,
+ this.filePathParts.join(kPathSeparator),
+ this.testPathParts.join(kPathSeparator),
+ stringifyPublicParams(this.params, true),
+ ];
+ }
+}
+
+/**
+ * A multi-case test query, like `s:f:t:` or `s:f:t:a=1,b=1`.
+ *
+ * Immutable (makes copies of constructor args).
+ */
+export class TestQuerySingleCase extends TestQueryMultiCase {
+ readonly level: TestQueryLevel = 4;
+ readonly isMultiCase: false = false;
+
+ get depthInLevel() {
+ return 0;
+ }
+
+ protected toStringHelper(): string[] {
+ return [
+ this.suite,
+ this.filePathParts.join(kPathSeparator),
+ this.testPathParts.join(kPathSeparator),
+ stringifyPublicParams(this.params),
+ ];
+ }
+}
+
+/**
+ * Parse raw expectations input into TestQueryWithExpectation[], filtering so that only
+ * expectations that are relevant for the provided query and wptURL.
+ *
+ * `rawExpectations` should be @type {{ query: string, expectation: Expectation }[]}
+ *
+ * The `rawExpectations` are parsed and validated that they are in the correct format.
+ * If `wptURL` is passed, the query string should be of the full path format such
+ * as `path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;*`.
+ * If `wptURL` is `undefined`, the query string should be only the query
+ * `suite:test_path:test_name:foo=1;bar=2;*`.
+ */
+export function parseExpectationsForTestQuery(
+ rawExpectations:
+ | unknown
+ | {
+ query: string;
+ expectation: Expectation;
+ }[],
+ query: TestQuery,
+ wptURL?: URL
+) {
+ if (!Array.isArray(rawExpectations)) {
+ unreachable('Expectations should be an array');
+ }
+ const expectations: TestQueryWithExpectation[] = [];
+ for (const entry of rawExpectations) {
+ assert(typeof entry === 'object');
+ const rawExpectation = entry as { query?: string; expectation?: string };
+ assert(rawExpectation.query !== undefined, 'Expectation missing query string');
+ assert(rawExpectation.expectation !== undefined, 'Expectation missing expectation string');
+
+ let expectationQuery: TestQuery;
+ if (wptURL !== undefined) {
+ const expectationURL = new URL(`${wptURL.origin}/${entry.query}`);
+ if (expectationURL.pathname !== wptURL.pathname) {
+ continue;
+ }
+ assert(
+ expectationURL.pathname === wptURL.pathname,
+ `Invalid expectation path ${expectationURL.pathname}
+Expectation should be of the form path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;...
+ `
+ );
+
+ const params = expectationURL.searchParams;
+ if (optionEnabled('worker', params) !== optionEnabled('worker', wptURL.searchParams)) {
+ continue;
+ }
+
+ const qs = params.getAll('q');
+ assert(qs.length === 1, 'currently, there must be exactly one ?q= in the expectation string');
+ expectationQuery = parseQuery(qs[0]);
+ } else {
+ expectationQuery = parseQuery(entry.query);
+ }
+
+ // Strip params from multicase expectations so that an expectation of foo=2;*
+ // is stored if the test query is bar=3;*
+ const queryForFilter =
+ expectationQuery instanceof TestQueryMultiCase
+ ? new TestQueryMultiCase(
+ expectationQuery.suite,
+ expectationQuery.filePathParts,
+ expectationQuery.testPathParts,
+ {}
+ )
+ : expectationQuery;
+
+ if (compareQueries(query, queryForFilter) === Ordering.Unordered) {
+ continue;
+ }
+
+ switch (entry.expectation) {
+ case 'pass':
+ case 'skip':
+ case 'fail':
+ break;
+ default:
+ unreachable(`Invalid expectation ${entry.expectation}`);
+ }
+
+ expectations.push({
+ query: expectationQuery,
+ expectation: entry.expectation,
+ });
+ }
+ return expectations;
+}
+
+/**
+ * For display purposes only, produces a "relative" query string from parent to child.
+ * Used in the wpt runtime to reduce the verbosity of logs.
+ */
+export function relativeQueryString(parent: TestQuery, child: TestQuery): string {
+ const ordering = compareQueries(parent, child);
+ if (ordering === Ordering.Equal) {
+ return '';
+ } else if (ordering === Ordering.StrictSuperset) {
+ const parentString = parent.toString();
+ assert(parentString.endsWith(kWildcard));
+ const childString = child.toString();
+ assert(
+ childString.startsWith(parentString.substring(0, parentString.length - 2)),
+ 'impossible?: childString does not start with parentString[:-2]'
+ );
+ return childString.substring(parentString.length - 2);
+ } else {
+ unreachable(
+ `relativeQueryString arguments have invalid ordering ${ordering}:\n${parent}\n${child}`
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/separators.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/separators.ts
new file mode 100644
index 0000000000..0c8f6ea9a9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/separators.ts
@@ -0,0 +1,14 @@
+/** Separator between big parts: suite:file:test:case */
+export const kBigSeparator = ':';
+
+/** Separator between path,to,file or path,to,test */
+export const kPathSeparator = ',';
+
+/** Separator between k=v;k=v */
+export const kParamSeparator = ';';
+
+/** Separator between key and value in k=v */
+export const kParamKVSeparator = '=';
+
+/** Final wildcard, if query is not single-case */
+export const kWildcard = '*';
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/stringify_params.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/stringify_params.ts
new file mode 100644
index 0000000000..907cc0791a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/stringify_params.ts
@@ -0,0 +1,44 @@
+import { TestParams } from '../../framework/fixture.js';
+import { assert } from '../../util/util.js';
+import { JSONWithUndefined, badParamValueChars, paramKeyIsPublic } from '../params_utils.js';
+
+import { stringifyParamValue, stringifyParamValueUniquely } from './json_param_value.js';
+import { kParamKVSeparator, kParamSeparator, kWildcard } from './separators.js';
+
+export function stringifyPublicParams(p: TestParams, addWildcard = false): string {
+ const parts = Object.keys(p)
+ .filter(k => paramKeyIsPublic(k))
+ .map(k => stringifySingleParam(k, p[k]));
+
+ if (addWildcard) parts.push(kWildcard);
+
+ return parts.join(kParamSeparator);
+}
+
+/**
+ * An _approximately_ unique string representing a CaseParams value.
+ */
+export function stringifyPublicParamsUniquely(p: TestParams): string {
+ const keys = Object.keys(p).sort();
+ return keys
+ .filter(k => paramKeyIsPublic(k))
+ .map(k => stringifySingleParamUniquely(k, p[k]))
+ .join(kParamSeparator);
+}
+
+export function stringifySingleParam(k: string, v: JSONWithUndefined) {
+ return `${k}${kParamKVSeparator}${stringifySingleParamValue(v)}`;
+}
+
+function stringifySingleParamUniquely(k: string, v: JSONWithUndefined) {
+ return `${k}${kParamKVSeparator}${stringifyParamValueUniquely(v)}`;
+}
+
+function stringifySingleParamValue(v: JSONWithUndefined): string {
+ const s = stringifyParamValue(v);
+ assert(
+ !badParamValueChars.test(s),
+ `JSON.stringified param value must not match ${badParamValueChars} - was ${s}`
+ );
+ return s;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/validQueryPart.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/validQueryPart.ts
new file mode 100644
index 0000000000..62184adb62
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/validQueryPart.ts
@@ -0,0 +1,2 @@
+/** Applies to group parts, test parts, params keys. */
+export const validQueryPart = /^[a-zA-Z0-9_]+$/;
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/stack.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/stack.ts
new file mode 100644
index 0000000000..5de54088c8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/stack.ts
@@ -0,0 +1,82 @@
+// Returns the stack trace of an Error, but without the extra boilerplate at the bottom
+// (e.g. RunCaseSpecific, processTicksAndRejections, etc.), for logging.
+export function extractImportantStackTrace(e: Error): string {
+ let stack = e.stack;
+ if (!stack) {
+ return '';
+ }
+ const redundantMessage = 'Error: ' + e.message + '\n';
+ if (stack.startsWith(redundantMessage)) {
+ stack = stack.substring(redundantMessage.length);
+ }
+
+ const lines = stack.split('\n');
+ for (let i = lines.length - 1; i >= 0; --i) {
+ const line = lines[i];
+ if (line.indexOf('.spec.') !== -1) {
+ return lines.slice(0, i + 1).join('\n');
+ }
+ }
+ return stack;
+}
+
+// *** Examples ***
+//
+// Node fail()
+// > Error:
+// > at CaseRecorder.fail (/Users/kainino/src/cts/src/common/framework/logger.ts:99:30)
+// > at RunCaseSpecific.exports.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/logger.spec.ts:80:7)
+// x at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+// x at processTicksAndRejections (internal/process/task_queues.js:86:5)
+//
+// Node throw
+// > Error: hello
+// > at RunCaseSpecific.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/test_group.spec.ts:51:11)
+// x at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+// x at processTicksAndRejections (internal/process/task_queues.js:86:5)
+//
+// Firefox fail()
+// > fail@http://localhost:8080/out/framework/logger.js:104:30
+// > expect@http://localhost:8080/out/framework/default_fixture.js:59:16
+// > @http://localhost:8080/out/unittests/util.spec.js:35:5
+// x run@http://localhost:8080/out/framework/test_group.js:119:18
+//
+// Firefox throw
+// > @http://localhost:8080/out/unittests/test_group.spec.js:48:11
+// x run@http://localhost:8080/out/framework/test_group.js:119:18
+//
+// Safari fail()
+// > fail@http://localhost:8080/out/framework/logger.js:104:39
+// > expect@http://localhost:8080/out/framework/default_fixture.js:59:20
+// > http://localhost:8080/out/unittests/util.spec.js:35:11
+// x http://localhost:8080/out/framework/test_group.js:119:20
+// x asyncFunctionResume@[native code]
+// x [native code]
+// x promiseReactionJob@[native code]
+//
+// Safari throw
+// > http://localhost:8080/out/unittests/test_group.spec.js:48:20
+// x http://localhost:8080/out/framework/test_group.js:119:20
+// x asyncFunctionResume@[native code]
+// x [native code]
+// x promiseReactionJob@[native code]
+//
+// Chrome fail()
+// x Error
+// x at CaseRecorder.fail (http://localhost:8080/out/framework/logger.js:104:30)
+// x at DefaultFixture.expect (http://localhost:8080/out/framework/default_fixture.js:59:16)
+// > at RunCaseSpecific.fn (http://localhost:8080/out/unittests/util.spec.js:35:5)
+// x at RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:18)
+// x at async runCase (http://localhost:8080/out/runtime/standalone.js:37:17)
+// x at async http://localhost:8080/out/runtime/standalone.js:102:7
+//
+// Chrome throw
+// x Error: hello
+// > at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+// x at RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:18)"
+// x at async Promise.all (index 0)
+// x at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
+// x at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
+// x at async RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:7)
+// x at async runCase (http://localhost:8080/out/runtime/standalone.js:37:17)
+// x at async http://localhost:8080/out/runtime/standalone.js:102:7
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts
new file mode 100644
index 0000000000..63f017083c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts
@@ -0,0 +1,646 @@
+import {
+ Fixture,
+ SubcaseBatchState,
+ SkipTestCase,
+ TestParams,
+ UnexpectedPassError,
+} from '../framework/fixture.js';
+import {
+ CaseParamsBuilder,
+ builderIterateCasesWithSubcases,
+ kUnitCaseParamsBuilder,
+ ParamsBuilderBase,
+ SubcaseParamsBuilder,
+} from '../framework/params_builder.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { Expectation } from '../internal/logging/result.js';
+import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
+import { extractPublicParams, Merged, mergeParams } from '../internal/params_utils.js';
+import { compareQueries, Ordering } from '../internal/query/compare.js';
+import { TestQuerySingleCase, TestQueryWithExpectation } from '../internal/query/query.js';
+import { kPathSeparator } from '../internal/query/separators.js';
+import {
+ stringifyPublicParams,
+ stringifyPublicParamsUniquely,
+} from '../internal/query/stringify_params.js';
+import { validQueryPart } from '../internal/query/validQueryPart.js';
+import { assert, unreachable } from '../util/util.js';
+
+export type RunFn = (
+ rec: TestCaseRecorder,
+ expectations?: TestQueryWithExpectation[]
+) => Promise<void>;
+
+export interface TestCaseID {
+ readonly test: readonly string[];
+ readonly params: TestParams;
+}
+
+export interface RunCase {
+ readonly id: TestCaseID;
+ readonly isUnimplemented: boolean;
+ run(
+ rec: TestCaseRecorder,
+ selfQuery: TestQuerySingleCase,
+ expectations: TestQueryWithExpectation[]
+ ): Promise<void>;
+}
+
+// Interface for defining tests
+export interface TestGroupBuilder<S extends SubcaseBatchState, F extends Fixture<S>> {
+ test(name: string): TestBuilderWithName<S, F>;
+}
+export function makeTestGroup<S extends SubcaseBatchState, F extends Fixture<S>>(
+ fixture: FixtureClass<S, F>
+): TestGroupBuilder<S, F> {
+ return new TestGroup((fixture as unknown) as FixtureClass);
+}
+
+// Interfaces for running tests
+export interface IterableTestGroup {
+ iterate(): Iterable<IterableTest>;
+ validate(): void;
+}
+export interface IterableTest {
+ testPath: string[];
+ description: string | undefined;
+ readonly testCreationStack: Error;
+ iterate(): Iterable<RunCase>;
+}
+
+export function makeTestGroupForUnitTesting<F extends Fixture>(
+ fixture: FixtureClass<SubcaseBatchState, F>
+): TestGroup<SubcaseBatchState, F> {
+ return new TestGroup(fixture);
+}
+
+export type FixtureClass<
+ S extends SubcaseBatchState = SubcaseBatchState,
+ F extends Fixture<S> = Fixture<S>
+> = {
+ new (sharedState: S, log: TestCaseRecorder, params: TestParams): F;
+ MakeSharedState(params: TestParams): S;
+};
+type TestFn<F extends Fixture, P extends {}> = (t: F & { params: P }) => Promise<void> | void;
+type BeforeAllSubcasesFn<S extends SubcaseBatchState, P extends {}> = (
+ s: S & { params: P }
+) => Promise<void> | void;
+
+export class TestGroup<S extends SubcaseBatchState, F extends Fixture<S>>
+ implements TestGroupBuilder<S, F> {
+ private fixture: FixtureClass;
+ private seen: Set<string> = new Set();
+ private tests: Array<TestBuilder<S, F>> = [];
+
+ constructor(fixture: FixtureClass) {
+ this.fixture = fixture;
+ }
+
+ iterate(): Iterable<IterableTest> {
+ return this.tests;
+ }
+
+ private checkName(name: string): void {
+ assert(
+ // Shouldn't happen due to the rule above. Just makes sure that treating
+ // unencoded strings as encoded strings is OK.
+ name === decodeURIComponent(name),
+ `Not decodeURIComponent-idempotent: ${name} !== ${decodeURIComponent(name)}`
+ );
+ assert(!this.seen.has(name), `Duplicate test name: ${name}`);
+
+ this.seen.add(name);
+ }
+
+ test(name: string): TestBuilderWithName<S, F> {
+ const testCreationStack = new Error(`Test created: ${name}`);
+
+ this.checkName(name);
+
+ const parts = name.split(kPathSeparator);
+ for (const p of parts) {
+ assert(validQueryPart.test(p), `Invalid test name part ${p}; must match ${validQueryPart}`);
+ }
+
+ const test = new TestBuilder(parts, this.fixture, testCreationStack);
+ this.tests.push(test);
+ return (test as unknown) as TestBuilderWithName<S, F>;
+ }
+
+ validate(): void {
+ for (const test of this.tests) {
+ test.validate();
+ }
+ }
+}
+
+interface TestBuilderWithName<S extends SubcaseBatchState, F extends Fixture<S>>
+ extends TestBuilderWithParams<S, F, {}, {}> {
+ desc(description: string): this;
+ /**
+ * A noop function to associate a test with the relevant part of the specification.
+ *
+ * @param url a link to the spec where test is extracted from.
+ */
+ specURL(url: string): this;
+ /**
+ * Parameterize the test, generating multiple cases, each possibly having subcases.
+ *
+ * The `unit` value passed to the `cases` callback is an immutable constant
+ * `CaseParamsBuilder<{}>` representing the "unit" builder `[ {} ]`,
+ * provided for convenience. The non-callback overload can be used if `unit` is not needed.
+ */
+ params<CaseP extends {}, SubcaseP extends {}>(
+ cases: (unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<CaseP, SubcaseP>
+ ): TestBuilderWithParams<S, F, CaseP, SubcaseP>;
+ /**
+ * Parameterize the test, generating multiple cases, each possibly having subcases.
+ *
+ * Use the callback overload of this method if a "unit" builder is needed.
+ */
+ params<CaseP extends {}, SubcaseP extends {}>(
+ cases: ParamsBuilderBase<CaseP, SubcaseP>
+ ): TestBuilderWithParams<S, F, CaseP, SubcaseP>;
+
+ /**
+ * Parameterize the test, generating multiple cases, without subcases.
+ */
+ paramsSimple<P extends {}>(cases: Iterable<P>): TestBuilderWithParams<S, F, P, {}>;
+
+ /**
+ * Parameterize the test, generating one case with multiple subcases.
+ */
+ paramsSubcasesOnly<P extends {}>(subcases: Iterable<P>): TestBuilderWithParams<S, F, {}, P>;
+ /**
+ * Parameterize the test, generating one case with multiple subcases.
+ *
+ * The `unit` value passed to the `subcases` callback is an immutable constant
+ * `SubcaseParamsBuilder<{}>`, with one empty case `{}` and one empty subcase `{}`.
+ */
+ paramsSubcasesOnly<P extends {}>(
+ subcases: (unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, P>
+ ): TestBuilderWithParams<S, F, {}, P>;
+}
+
+interface TestBuilderWithParams<
+ S extends SubcaseBatchState,
+ F extends Fixture<S>,
+ CaseP extends {},
+ SubcaseP extends {}
+> {
+ /**
+ * Limit subcases to a maximum number of per testcase.
+ * @param b the maximum number of subcases per testcase.
+ *
+ * If the number of subcases exceeds `b`, add an internal
+ * numeric, incrementing `batch__` param to split subcases
+ * into groups of at most `b` subcases.
+ */
+ batch(b: number): this;
+ /**
+ * Run a function on shared subcase batch state before each
+ * batch of subcases.
+ * @param fn the function to run. It is called with the test
+ * fixture's shared subcase batch state.
+ *
+ * Generally, this function should be careful to avoid mutating
+ * any state on the shared subcase batch state which could result
+ * in unexpected order-dependent test behavior.
+ */
+ beforeAllSubcases(fn: BeforeAllSubcasesFn<S, CaseP>): this;
+ /**
+ * Set the test function.
+ * @param fn the test function.
+ */
+ fn(fn: TestFn<F, Merged<CaseP, SubcaseP>>): void;
+ /**
+ * Mark the test as unimplemented.
+ */
+ unimplemented(): void;
+}
+
+class TestBuilder<S extends SubcaseBatchState, F extends Fixture> {
+ readonly testPath: string[];
+ isUnimplemented: boolean;
+ description: string | undefined;
+ readonly testCreationStack: Error;
+
+ private readonly fixture: FixtureClass;
+ private testFn: TestFn<Fixture, {}> | undefined;
+ private beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined;
+ private testCases?: ParamsBuilderBase<{}, {}> = undefined;
+ private batchSize: number = 0;
+
+ constructor(testPath: string[], fixture: FixtureClass, testCreationStack: Error) {
+ this.testPath = testPath;
+ this.isUnimplemented = false;
+ this.fixture = fixture;
+ this.testCreationStack = testCreationStack;
+ }
+
+ desc(description: string): this {
+ this.description = description.trim();
+ return this;
+ }
+
+ specURL(url: string): this {
+ return this;
+ }
+
+ beforeAllSubcases(fn: BeforeAllSubcasesFn<SubcaseBatchState, {}>): this {
+ assert(this.beforeFn === undefined);
+ this.beforeFn = fn;
+ return this;
+ }
+
+ fn(fn: TestFn<Fixture, {}>): void {
+ // eslint-disable-next-line no-warning-comments
+ // MAINTENANCE_TODO: add "TODO" if there's no description? (and make sure it only ends up on
+ // actual tests, not on test parents in the tree, which is what happens if you do it here, not
+ // sure why)
+ assert(this.testFn === undefined);
+ this.testFn = fn;
+ }
+
+ batch(b: number): this {
+ this.batchSize = b;
+ return this;
+ }
+
+ unimplemented(): void {
+ assert(this.testFn === undefined);
+
+ this.description =
+ (this.description ? this.description + '\n\n' : '') + 'TODO: .unimplemented()';
+ this.isUnimplemented = true;
+
+ this.testFn = () => {
+ throw new SkipTestCase('test unimplemented');
+ };
+ }
+
+ validate(): void {
+ const testPathString = this.testPath.join(kPathSeparator);
+ assert(this.testFn !== undefined, () => {
+ let s = `Test is missing .fn(): ${testPathString}`;
+ if (this.testCreationStack.stack) {
+ s += `\n-> test created at:\n${this.testCreationStack.stack}`;
+ }
+ return s;
+ });
+
+ if (this.testCases === undefined) {
+ return;
+ }
+
+ const seen = new Set<string>();
+ for (const [caseParams, subcases] of builderIterateCasesWithSubcases(this.testCases)) {
+ for (const subcaseParams of subcases ?? [{}]) {
+ const params = mergeParams(caseParams, subcaseParams);
+ assert(this.batchSize === 0 || !('batch__' in params));
+
+ // stringifyPublicParams also checks for invalid params values
+ const testcaseString = stringifyPublicParams(params);
+
+ // A (hopefully) unique representation of a params value.
+ const testcaseStringUnique = stringifyPublicParamsUniquely(params);
+ assert(
+ !seen.has(testcaseStringUnique),
+ `Duplicate public test case params for test ${testPathString}: ${testcaseString}`
+ );
+ seen.add(testcaseStringUnique);
+ }
+ }
+ }
+
+ params(
+ cases: ((unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<{}, {}>) | ParamsBuilderBase<{}, {}>
+ ): TestBuilder<S, F> {
+ assert(this.testCases === undefined, 'test case is already parameterized');
+ if (cases instanceof Function) {
+ this.testCases = cases(kUnitCaseParamsBuilder);
+ } else {
+ this.testCases = cases;
+ }
+ return this;
+ }
+
+ paramsSimple(cases: Iterable<{}>): TestBuilder<S, F> {
+ assert(this.testCases === undefined, 'test case is already parameterized');
+ this.testCases = kUnitCaseParamsBuilder.combineWithParams(cases);
+ return this;
+ }
+
+ paramsSubcasesOnly(
+ subcases: Iterable<{}> | ((unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, {}>)
+ ): TestBuilder<S, F> {
+ if (subcases instanceof Function) {
+ return this.params(subcases(kUnitCaseParamsBuilder.beginSubcases()));
+ } else {
+ return this.params(kUnitCaseParamsBuilder.beginSubcases().combineWithParams(subcases));
+ }
+ }
+
+ *iterate(): IterableIterator<RunCase> {
+ assert(this.testFn !== undefined, 'No test function (.fn()) for test');
+ this.testCases ??= kUnitCaseParamsBuilder;
+ for (const [caseParams, subcases] of builderIterateCasesWithSubcases(this.testCases)) {
+ if (this.batchSize === 0 || subcases === undefined) {
+ yield new RunCaseSpecific(
+ this.testPath,
+ caseParams,
+ this.isUnimplemented,
+ subcases,
+ this.fixture,
+ this.testFn,
+ this.beforeFn,
+ this.testCreationStack
+ );
+ } else {
+ const subcaseArray = Array.from(subcases);
+ if (subcaseArray.length <= this.batchSize) {
+ yield new RunCaseSpecific(
+ this.testPath,
+ caseParams,
+ this.isUnimplemented,
+ subcaseArray,
+ this.fixture,
+ this.testFn,
+ this.beforeFn,
+ this.testCreationStack
+ );
+ } else {
+ for (let i = 0; i < subcaseArray.length; i = i + this.batchSize) {
+ yield new RunCaseSpecific(
+ this.testPath,
+ { ...caseParams, batch__: i / this.batchSize },
+ this.isUnimplemented,
+ subcaseArray.slice(i, Math.min(subcaseArray.length, i + this.batchSize)),
+ this.fixture,
+ this.testFn,
+ this.beforeFn,
+ this.testCreationStack
+ );
+ }
+ }
+ }
+ }
+ }
+}
+
+class RunCaseSpecific implements RunCase {
+ readonly id: TestCaseID;
+ readonly isUnimplemented: boolean;
+
+ private readonly params: {};
+ private readonly subcases: Iterable<{}> | undefined;
+ private readonly fixture: FixtureClass;
+ private readonly fn: TestFn<Fixture, {}>;
+ private readonly beforeFn?: BeforeAllSubcasesFn<SubcaseBatchState, {}>;
+ private readonly testCreationStack: Error;
+
+ constructor(
+ testPath: string[],
+ params: {},
+ isUnimplemented: boolean,
+ subcases: Iterable<{}> | undefined,
+ fixture: FixtureClass,
+ fn: TestFn<Fixture, {}>,
+ beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined,
+ testCreationStack: Error
+ ) {
+ this.id = { test: testPath, params: extractPublicParams(params) };
+ this.isUnimplemented = isUnimplemented;
+ this.params = params;
+ this.subcases = subcases;
+ this.fixture = fixture;
+ this.fn = fn;
+ this.beforeFn = beforeFn;
+ this.testCreationStack = testCreationStack;
+ }
+
+ async runTest(
+ rec: TestCaseRecorder,
+ sharedState: SubcaseBatchState,
+ params: TestParams,
+ throwSkip: boolean,
+ expectedStatus: Expectation
+ ): Promise<void> {
+ try {
+ rec.beginSubCase();
+ if (expectedStatus === 'skip') {
+ throw new SkipTestCase('Skipped by expectations');
+ }
+
+ const inst = new this.fixture(sharedState, rec, params);
+ try {
+ await inst.init();
+ await this.fn(inst as Fixture & { params: {} });
+ } finally {
+ // Runs as long as constructor succeeded, even if initialization or the test failed.
+ await inst.finalize();
+ }
+ } catch (ex) {
+ // There was an exception from constructor, init, test, or finalize.
+ // An error from init or test may have been a SkipTestCase.
+ // An error from finalize may have been an eventualAsyncExpectation failure
+ // or unexpected validation/OOM error from the GPUDevice.
+ if (throwSkip && ex instanceof SkipTestCase) {
+ throw ex;
+ }
+ rec.threw(ex);
+ } finally {
+ try {
+ rec.endSubCase(expectedStatus);
+ } catch (ex) {
+ assert(ex instanceof UnexpectedPassError);
+ ex.message = `Testcase passed unexpectedly.`;
+ ex.stack = this.testCreationStack.stack;
+ rec.warn(ex);
+ }
+ }
+ }
+
+ async run(
+ rec: TestCaseRecorder,
+ selfQuery: TestQuerySingleCase,
+ expectations: TestQueryWithExpectation[]
+ ): Promise<void> {
+ const getExpectedStatus = (selfQueryWithSubParams: TestQuerySingleCase) => {
+ let didSeeFail = false;
+ for (const exp of expectations) {
+ const ordering = compareQueries(exp.query, selfQueryWithSubParams);
+ if (ordering === Ordering.Unordered || ordering === Ordering.StrictSubset) {
+ continue;
+ }
+
+ switch (exp.expectation) {
+ // Skip takes precedence. If there is any expectation indicating a skip,
+ // signal it immediately.
+ case 'skip':
+ return 'skip';
+ case 'fail':
+ // Otherwise, indicate that we might expect a failure.
+ didSeeFail = true;
+ break;
+ default:
+ unreachable();
+ }
+ }
+ return didSeeFail ? 'fail' : 'pass';
+ };
+
+ const { testHeartbeatCallback, maxSubcasesInFlight } = globalTestConfig;
+ try {
+ rec.start();
+ const sharedState = this.fixture.MakeSharedState(this.params);
+ try {
+ await sharedState.init();
+ if (this.beforeFn) {
+ await this.beforeFn(sharedState);
+ }
+ await sharedState.postInit();
+ testHeartbeatCallback();
+
+ let allPreviousSubcasesFinalizedPromise: Promise<void> = Promise.resolve();
+ if (this.subcases) {
+ let totalCount = 0;
+ let skipCount = 0;
+
+ // If there are too many subcases in flight, starting the next subcase will register
+ // `resolvePromiseBlockingSubcase` and wait until `subcaseFinishedCallback` is called.
+ let subcasesInFlight = 0;
+ let resolvePromiseBlockingSubcase: (() => void) | undefined = undefined;
+ const subcaseFinishedCallback = () => {
+ subcasesInFlight -= 1;
+ // If there is any subcase waiting on a previous subcase to finish,
+ // unblock it now, and clear the resolve callback.
+ if (resolvePromiseBlockingSubcase) {
+ resolvePromiseBlockingSubcase();
+ resolvePromiseBlockingSubcase = undefined;
+ }
+ };
+
+ for (const subParams of this.subcases) {
+ // Make a recorder that will defer all calls until `allPreviousSubcasesFinalizedPromise`
+ // resolves. Waiting on `allPreviousSubcasesFinalizedPromise` ensures that
+ // logs from all the previous subcases have been flushed before flushing new logs.
+ const subcasePrefix = 'subcase: ' + stringifyPublicParams(subParams);
+ const subRec = new Proxy(rec, {
+ get: (target, k: keyof TestCaseRecorder) => {
+ const prop = TestCaseRecorder.prototype[k];
+ if (typeof prop === 'function') {
+ testHeartbeatCallback();
+ return function (...args: Parameters<typeof prop>) {
+ void allPreviousSubcasesFinalizedPromise.then(() => {
+ // Prepend the subcase name to all error messages.
+ for (const arg of args) {
+ if (arg instanceof Error) {
+ try {
+ arg.message = subcasePrefix + '\n' + arg.message;
+ } catch {
+ // If that fails (e.g. on DOMException), try to put it in the stack:
+ let stack = subcasePrefix;
+ if (arg.stack) stack += '\n' + arg.stack;
+ try {
+ arg.stack = stack;
+ } catch {
+ // If that fails too, just silence it.
+ }
+ }
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const rv = (prop as any).apply(target, args);
+ // Because this proxy executes functions in a deferred manner,
+ // it should never be used for functions that need to return a value.
+ assert(rv === undefined);
+ });
+ };
+ }
+ return prop;
+ },
+ });
+
+ const params = mergeParams(this.params, subParams);
+ const subcaseQuery = new TestQuerySingleCase(
+ selfQuery.suite,
+ selfQuery.filePathParts,
+ selfQuery.testPathParts,
+ params
+ );
+
+ // Limit the maximum number of subcases in flight.
+ if (subcasesInFlight >= maxSubcasesInFlight) {
+ await new Promise<void>(resolve => {
+ // There should only be one subcase waiting at a time.
+ assert(resolvePromiseBlockingSubcase === undefined);
+ resolvePromiseBlockingSubcase = resolve;
+ });
+ }
+
+ subcasesInFlight += 1;
+ // Runs async without waiting so that subsequent subcases can start.
+ // All finalization steps will be waited on at the end of the testcase.
+ const finalizePromise = this.runTest(
+ subRec,
+ sharedState,
+ params,
+ /* throwSkip */ true,
+ getExpectedStatus(subcaseQuery)
+ )
+ .then(() => {
+ subRec.info(new Error('OK'));
+ })
+ .catch(ex => {
+ if (ex instanceof SkipTestCase) {
+ // Convert SkipTestCase to info messages
+ ex.message = 'subcase skipped: ' + ex.message;
+ subRec.info(ex);
+ ++skipCount;
+ } else {
+ // Since we are catching all error inside runTest(), this should never happen
+ subRec.threw(ex);
+ }
+ })
+ .finally(subcaseFinishedCallback);
+
+ allPreviousSubcasesFinalizedPromise = allPreviousSubcasesFinalizedPromise.then(
+ () => finalizePromise
+ );
+ ++totalCount;
+ }
+
+ // Wait for all subcases to finalize and report their results.
+ await allPreviousSubcasesFinalizedPromise;
+
+ if (skipCount === totalCount) {
+ rec.skipped(new SkipTestCase('all subcases were skipped'));
+ }
+ } else {
+ await this.runTest(
+ rec,
+ sharedState,
+ this.params,
+ /* throwSkip */ false,
+ getExpectedStatus(selfQuery)
+ );
+ }
+ } finally {
+ testHeartbeatCallback();
+ // Runs as long as the shared state constructor succeeded, even if initialization or a test failed.
+ await sharedState.finalize();
+ testHeartbeatCallback();
+ }
+ } catch (ex) {
+ // There was an exception from sharedState/fixture constructor, init, beforeFn, or test.
+ // An error from beforeFn may have been SkipTestCase.
+ // An error from finalize may have been an eventualAsyncExpectation failure
+ // or unexpected validation/OOM error from the GPUDevice.
+ rec.threw(ex);
+ } finally {
+ rec.finish();
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts
new file mode 100644
index 0000000000..2d2b555366
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts
@@ -0,0 +1,15 @@
+// A listing of all specs within a single suite. This is the (awaited) type of
+// `groups` in '{cts,unittests}/listing.ts' and `listing` in the auto-generated
+// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings).
+export type TestSuiteListing = TestSuiteListingEntry[];
+
+export type TestSuiteListingEntry = TestSuiteListingEntrySpec | TestSuiteListingEntryReadme;
+
+interface TestSuiteListingEntrySpec {
+ readonly file: string[];
+}
+
+interface TestSuiteListingEntryReadme {
+ readonly file: string[];
+ readonly readme: string;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts
new file mode 100644
index 0000000000..204a4f693a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts
@@ -0,0 +1,575 @@
+import { RunCase, RunFn } from '../internal/test_group.js';
+import { assert } from '../util/util.js';
+
+import { TestFileLoader } from './file_loader.js';
+import { TestParamsRW } from './params_utils.js';
+import { compareQueries, Ordering } from './query/compare.js';
+import {
+ TestQuery,
+ TestQueryMultiCase,
+ TestQuerySingleCase,
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+} from './query/query.js';
+import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './query/separators.js';
+import { stringifySingleParam } from './query/stringify_params.js';
+import { StacklessError } from './util.js';
+
+// `loadTreeForQuery()` loads a TestTree for a given queryToLoad.
+// The resulting tree is a linked-list all the way from `suite:*` to queryToLoad,
+// and under queryToLoad is a tree containing every case matched by queryToLoad.
+//
+// `subqueriesToExpand` influences the `collapsible` flag on nodes in the resulting tree.
+// A node is considered "collapsible" if none of the subqueriesToExpand is a StrictSubset
+// of that node.
+//
+// In WebKit/Blink-style web_tests, an expectation file marks individual cts.https.html "variants
+// as "Failure", "Crash", etc. By passing in the list of expectations as the subqueriesToExpand,
+// we can programmatically subdivide the cts.https.html "variants" list to be able to implement
+// arbitrarily-fine suppressions (instead of having to suppress entire test files, which would
+// lose a lot of coverage).
+//
+// `iterateCollapsedNodes()` produces the list of queries for the variants list.
+//
+// Though somewhat complicated, this system has important benefits:
+// - Avoids having to suppress entire test files, which would cause large test coverage loss.
+// - Minimizes the number of page loads needed for fine-grained suppressions.
+// (In the naive case, we could do one page load per test case - but the test suite would
+// take impossibly long to run.)
+// - Enables developers to put any number of tests in one file as appropriate, without worrying
+// about expectation granularity.
+
+interface TestTreeNodeBase<T extends TestQuery> {
+ readonly query: T;
+ /**
+ * Readable "relative" name for display in standalone runner.
+ * Not always the exact relative name, because sometimes there isn't
+ * one (e.g. s:f:* relative to s:f,*), but something that is readable.
+ */
+ readonly readableRelativeName: string;
+ subtreeCounts?: { tests: number; nodesWithTODO: number };
+}
+
+export interface TestSubtree<T extends TestQuery = TestQuery> extends TestTreeNodeBase<T> {
+ readonly children: Map<string, TestTreeNode>;
+ readonly collapsible: boolean;
+ description?: string;
+ readonly testCreationStack?: Error;
+}
+
+export interface TestTreeLeaf extends TestTreeNodeBase<TestQuerySingleCase> {
+ readonly run: RunFn;
+ readonly isUnimplemented?: boolean;
+ subtreeCounts?: undefined;
+}
+
+export type TestTreeNode = TestSubtree | TestTreeLeaf;
+
+/**
+ * When iterating through "collapsed" tree nodes, indicates how many "query levels" to traverse
+ * through before starting to collapse nodes.
+ *
+ * Corresponds with TestQueryLevel, but excludes 4 (SingleCase):
+ * - 1 = MultiFile. Expands so every file is in the collapsed tree.
+ * - 2 = MultiTest. Expands so every test is in the collapsed tree.
+ * - 3 = MultiCase. Expands so every case is in the collapsed tree (i.e. collapsing disabled).
+ */
+export type ExpandThroughLevel = 1 | 2 | 3;
+
+export class TestTree {
+ /**
+ * The `queryToLoad` that this test tree was created for.
+ * Test trees are always rooted at `suite:*`, but they only contain nodes that fit
+ * within `forQuery`.
+ *
+ * This is used for `iterateCollapsedNodes` which only starts collapsing at the next
+ * `TestQueryLevel` after `forQuery`.
+ */
+ readonly forQuery: TestQuery;
+ readonly root: TestSubtree;
+
+ constructor(forQuery: TestQuery, root: TestSubtree) {
+ this.forQuery = forQuery;
+ TestTree.propagateCounts(root);
+ this.root = root;
+ assert(
+ root.query.level === 1 && root.query.depthInLevel === 0,
+ 'TestTree root must be the root (suite:*)'
+ );
+ }
+
+ /**
+ * Iterate through the leaves of a version of the tree which has been pruned to exclude
+ * subtrees which:
+ * - are at a deeper `TestQueryLevel` than `this.forQuery`, and
+ * - were not a `Ordering.StrictSubset` of any of the `subqueriesToExpand` during tree creation.
+ */
+ iterateCollapsedNodes({
+ includeIntermediateNodes = false,
+ includeEmptySubtrees = false,
+ alwaysExpandThroughLevel,
+ }: {
+ /** Whether to include intermediate tree nodes or only collapsed-leaves. */
+ includeIntermediateNodes?: boolean;
+ /** Whether to include collapsed-leaves with no children. */
+ includeEmptySubtrees?: boolean;
+ /** Never collapse nodes up through this level. */
+ alwaysExpandThroughLevel: ExpandThroughLevel;
+ }): IterableIterator<Readonly<TestTreeNode>> {
+ const expandThroughLevel = Math.max(this.forQuery.level, alwaysExpandThroughLevel);
+ return TestTree.iterateSubtreeNodes(this.root, {
+ includeIntermediateNodes,
+ includeEmptySubtrees,
+ expandThroughLevel,
+ });
+ }
+
+ iterateLeaves(): IterableIterator<Readonly<TestTreeLeaf>> {
+ return TestTree.iterateSubtreeLeaves(this.root);
+ }
+
+ /**
+ * Dissolve nodes which have only one child, e.g.:
+ * a,* { a,b,* { a,b:* { ... } } }
+ * collapses down into:
+ * a,* { a,b:* { ... } }
+ * which is less needlessly verbose when displaying the tree in the standalone runner.
+ */
+ dissolveSingleChildTrees(): void {
+ const newRoot = dissolveSingleChildTrees(this.root);
+ assert(newRoot === this.root);
+ }
+
+ toString(): string {
+ return TestTree.subtreeToString('(root)', this.root, '');
+ }
+
+ static *iterateSubtreeNodes(
+ subtree: TestSubtree,
+ opts: {
+ includeIntermediateNodes: boolean;
+ includeEmptySubtrees: boolean;
+ expandThroughLevel: number;
+ }
+ ): IterableIterator<TestTreeNode> {
+ if (opts.includeIntermediateNodes) {
+ yield subtree;
+ }
+
+ for (const [, child] of subtree.children) {
+ if ('children' in child) {
+ // Is a subtree
+ const collapsible = child.collapsible && child.query.level > opts.expandThroughLevel;
+ if (child.children.size > 0 && !collapsible) {
+ yield* TestTree.iterateSubtreeNodes(child, opts);
+ } else if (child.children.size > 0 || opts.includeEmptySubtrees) {
+ // Don't yield empty subtrees (e.g. files with no tests) unless includeEmptySubtrees
+ yield child;
+ }
+ } else {
+ // Is a leaf
+ yield child;
+ }
+ }
+ }
+
+ static *iterateSubtreeLeaves(subtree: TestSubtree): IterableIterator<TestTreeLeaf> {
+ for (const [, child] of subtree.children) {
+ if ('children' in child) {
+ yield* TestTree.iterateSubtreeLeaves(child);
+ } else {
+ yield child;
+ }
+ }
+ }
+
+ /** Propagate the subtreeTODOs/subtreeTests state upward from leaves to parent nodes. */
+ static propagateCounts(subtree: TestSubtree): { tests: number; nodesWithTODO: number } {
+ subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0 };
+ for (const [, child] of subtree.children) {
+ if ('children' in child) {
+ const counts = TestTree.propagateCounts(child);
+ subtree.subtreeCounts.tests += counts.tests;
+ subtree.subtreeCounts.nodesWithTODO += counts.nodesWithTODO;
+ }
+ }
+ return subtree.subtreeCounts;
+ }
+
+ /** Displays counts in the format `(Nodes with TODOs) / (Total test count)`. */
+ static countsToString(tree: TestTreeNode): string {
+ if (tree.subtreeCounts) {
+ return `${tree.subtreeCounts.nodesWithTODO} / ${tree.subtreeCounts.tests}`;
+ } else {
+ return '';
+ }
+ }
+
+ static subtreeToString(name: string, tree: TestTreeNode, indent: string): string {
+ const collapsible = 'run' in tree ? '>' : tree.collapsible ? '+' : '-';
+ let s =
+ indent +
+ `${collapsible} ${TestTree.countsToString(tree)} ${JSON.stringify(name)} => ${tree.query}`;
+ if ('children' in tree) {
+ if (tree.description !== undefined) {
+ s += `\n${indent} | ${JSON.stringify(tree.description)}`;
+ }
+
+ for (const [name, child] of tree.children) {
+ s += '\n' + TestTree.subtreeToString(name, child, indent + ' ');
+ }
+ }
+ return s;
+ }
+}
+
+// MAINTENANCE_TODO: Consider having subqueriesToExpand actually impact the depth-order of params
+// in the tree.
+export async function loadTreeForQuery(
+ loader: TestFileLoader,
+ queryToLoad: TestQuery,
+ subqueriesToExpand: TestQuery[]
+): Promise<TestTree> {
+ const suite = queryToLoad.suite;
+ const specs = await loader.listing(suite);
+
+ const subqueriesToExpandEntries = Array.from(subqueriesToExpand.entries());
+ const seenSubqueriesToExpand: boolean[] = new Array(subqueriesToExpand.length);
+ seenSubqueriesToExpand.fill(false);
+
+ const isCollapsible = (subquery: TestQuery) =>
+ subqueriesToExpandEntries.every(([i, toExpand]) => {
+ const ordering = compareQueries(toExpand, subquery);
+
+ // If toExpand == subquery, no expansion is needed (but it's still "seen").
+ if (ordering === Ordering.Equal) seenSubqueriesToExpand[i] = true;
+ return ordering !== Ordering.StrictSubset;
+ });
+
+ // L0 = suite-level, e.g. suite:*
+ // L1 = file-level, e.g. suite:a,b:*
+ // L2 = test-level, e.g. suite:a,b:c,d:*
+ // L3 = case-level, e.g. suite:a,b:c,d:
+ let foundCase = false;
+ // L0 is suite:*
+ const subtreeL0 = makeTreeForSuite(suite, isCollapsible);
+ for (const entry of specs) {
+ if (entry.file.length === 0 && 'readme' in entry) {
+ // Suite-level readme.
+ setSubtreeDescriptionAndCountTODOs(subtreeL0, entry.readme);
+ continue;
+ }
+
+ {
+ const queryL1 = new TestQueryMultiFile(suite, entry.file);
+ const orderingL1 = compareQueries(queryL1, queryToLoad);
+ if (orderingL1 === Ordering.Unordered) {
+ // File path is not matched by this query.
+ continue;
+ }
+ }
+
+ if ('readme' in entry) {
+ // Entry is a README that is an ancestor or descendant of the query.
+ // (It's included for display in the standalone runner.)
+
+ // readmeSubtree is suite:a,b,*
+ // (This is always going to dedup with a file path, if there are any test spec files under
+ // the directory that has the README).
+ const readmeSubtree: TestSubtree<TestQueryMultiFile> = addSubtreeForDirPath(
+ subtreeL0,
+ entry.file,
+ isCollapsible
+ );
+ setSubtreeDescriptionAndCountTODOs(readmeSubtree, entry.readme);
+ continue;
+ }
+ // Entry is a spec file.
+
+ const spec = await loader.importSpecFile(queryToLoad.suite, entry.file);
+ // subtreeL1 is suite:a,b:*
+ const subtreeL1: TestSubtree<TestQueryMultiTest> = addSubtreeForFilePath(
+ subtreeL0,
+ entry.file,
+ isCollapsible
+ );
+ setSubtreeDescriptionAndCountTODOs(subtreeL1, spec.description);
+
+ let groupHasTests = false;
+ for (const t of spec.g.iterate()) {
+ groupHasTests = true;
+ {
+ const queryL2 = new TestQueryMultiCase(suite, entry.file, t.testPath, {});
+ const orderingL2 = compareQueries(queryL2, queryToLoad);
+ if (orderingL2 === Ordering.Unordered) {
+ // Test path is not matched by this query.
+ continue;
+ }
+ }
+
+ // subtreeL2 is suite:a,b:c,d:*
+ const subtreeL2: TestSubtree<TestQueryMultiCase> = addSubtreeForTestPath(
+ subtreeL1,
+ t.testPath,
+ t.testCreationStack,
+ isCollapsible
+ );
+ // This is 1 test. Set tests=1 then count TODOs.
+ subtreeL2.subtreeCounts ??= { tests: 1, nodesWithTODO: 0 };
+ if (t.description) setSubtreeDescriptionAndCountTODOs(subtreeL2, t.description);
+
+ // MAINTENANCE_TODO: If tree generation gets too slow, avoid actually iterating the cases in a
+ // file if there's no need to (based on the subqueriesToExpand).
+ for (const c of t.iterate()) {
+ {
+ const queryL3 = new TestQuerySingleCase(suite, entry.file, c.id.test, c.id.params);
+ const orderingL3 = compareQueries(queryL3, queryToLoad);
+ if (orderingL3 === Ordering.Unordered || orderingL3 === Ordering.StrictSuperset) {
+ // Case is not matched by this query.
+ continue;
+ }
+ }
+
+ // Leaf for case is suite:a,b:c,d:x=1;y=2
+ addLeafForCase(subtreeL2, c, isCollapsible);
+
+ foundCase = true;
+ }
+ }
+ if (!groupHasTests && !subtreeL1.subtreeCounts) {
+ throw new StacklessError(
+ `${subtreeL1.query} has no tests - it must have "TODO" in its description`
+ );
+ }
+ }
+
+ for (const [i, sq] of subqueriesToExpandEntries) {
+ const subquerySeen = seenSubqueriesToExpand[i];
+ if (!subquerySeen) {
+ throw new StacklessError(
+ `subqueriesToExpand entry did not match anything \
+(could be wrong, or could be redundant with a previous subquery):\n ${sq.toString()}`
+ );
+ }
+ }
+ assert(foundCase, `Query \`${queryToLoad.toString()}\` does not match any cases`);
+
+ return new TestTree(queryToLoad, subtreeL0);
+}
+
+function setSubtreeDescriptionAndCountTODOs(
+ subtree: TestSubtree<TestQueryMultiFile>,
+ description: string
+) {
+ assert(subtree.description === undefined);
+ subtree.description = description.trim();
+ subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0 };
+ if (subtree.description.indexOf('TODO') !== -1) {
+ subtree.subtreeCounts.nodesWithTODO++;
+ }
+}
+
+function makeTreeForSuite(
+ suite: string,
+ isCollapsible: (sq: TestQuery) => boolean
+): TestSubtree<TestQueryMultiFile> {
+ const query = new TestQueryMultiFile(suite, []);
+ return {
+ readableRelativeName: suite + kBigSeparator,
+ query,
+ children: new Map(),
+ collapsible: isCollapsible(query),
+ };
+}
+
+function addSubtreeForDirPath(
+ tree: TestSubtree<TestQueryMultiFile>,
+ file: string[],
+ isCollapsible: (sq: TestQuery) => boolean
+): TestSubtree<TestQueryMultiFile> {
+ const subqueryFile: string[] = [];
+ // To start, tree is suite:*
+ // This loop goes from that -> suite:a,* -> suite:a,b,*
+ for (const part of file) {
+ subqueryFile.push(part);
+ tree = getOrInsertSubtree(part, tree, () => {
+ const query = new TestQueryMultiFile(tree.query.suite, subqueryFile);
+ return {
+ readableRelativeName: part + kPathSeparator + kWildcard,
+ query,
+ collapsible: isCollapsible(query),
+ };
+ });
+ }
+ return tree;
+}
+
+function addSubtreeForFilePath(
+ tree: TestSubtree<TestQueryMultiFile>,
+ file: string[],
+ isCollapsible: (sq: TestQuery) => boolean
+): TestSubtree<TestQueryMultiTest> {
+ // To start, tree is suite:*
+ // This goes from that -> suite:a,* -> suite:a,b,*
+ tree = addSubtreeForDirPath(tree, file, isCollapsible);
+ // This goes from that -> suite:a,b:*
+ const subtree = getOrInsertSubtree('', tree, () => {
+ const query = new TestQueryMultiTest(tree.query.suite, tree.query.filePathParts, []);
+ assert(file.length > 0, 'file path is empty');
+ return {
+ readableRelativeName: file[file.length - 1] + kBigSeparator + kWildcard,
+ query,
+ collapsible: isCollapsible(query),
+ };
+ });
+ return subtree;
+}
+
+function addSubtreeForTestPath(
+ tree: TestSubtree<TestQueryMultiTest>,
+ test: readonly string[],
+ testCreationStack: Error,
+ isCollapsible: (sq: TestQuery) => boolean
+): TestSubtree<TestQueryMultiCase> {
+ const subqueryTest: string[] = [];
+ // To start, tree is suite:a,b:*
+ // This loop goes from that -> suite:a,b:c,* -> suite:a,b:c,d,*
+ for (const part of test) {
+ subqueryTest.push(part);
+ tree = getOrInsertSubtree(part, tree, () => {
+ const query = new TestQueryMultiTest(
+ tree.query.suite,
+ tree.query.filePathParts,
+ subqueryTest
+ );
+ return {
+ readableRelativeName: part + kPathSeparator + kWildcard,
+ query,
+ collapsible: isCollapsible(query),
+ };
+ });
+ }
+ // This goes from that -> suite:a,b:c,d:*
+ return getOrInsertSubtree('', tree, () => {
+ const query = new TestQueryMultiCase(
+ tree.query.suite,
+ tree.query.filePathParts,
+ subqueryTest,
+ {}
+ );
+ assert(subqueryTest.length > 0, 'subqueryTest is empty');
+ return {
+ readableRelativeName: subqueryTest[subqueryTest.length - 1] + kBigSeparator + kWildcard,
+ kWildcard,
+ query,
+ testCreationStack,
+ collapsible: isCollapsible(query),
+ };
+ });
+}
+
+function addLeafForCase(
+ tree: TestSubtree<TestQueryMultiTest>,
+ t: RunCase,
+ checkCollapsible: (sq: TestQuery) => boolean
+): void {
+ const query = tree.query;
+ let name: string = '';
+ const subqueryParams: TestParamsRW = {};
+
+ // To start, tree is suite:a,b:c,d:*
+ // This loop goes from that -> suite:a,b:c,d:x=1;* -> suite:a,b:c,d:x=1;y=2;*
+ for (const [k, v] of Object.entries(t.id.params)) {
+ name = stringifySingleParam(k, v);
+ subqueryParams[k] = v;
+
+ tree = getOrInsertSubtree(name, tree, () => {
+ const subquery = new TestQueryMultiCase(
+ query.suite,
+ query.filePathParts,
+ query.testPathParts,
+ subqueryParams
+ );
+ return {
+ readableRelativeName: name + kParamSeparator + kWildcard,
+ query: subquery,
+ collapsible: checkCollapsible(subquery),
+ };
+ });
+ }
+
+ // This goes from that -> suite:a,b:c,d:x=1;y=2
+ const subquery = new TestQuerySingleCase(
+ query.suite,
+ query.filePathParts,
+ query.testPathParts,
+ subqueryParams
+ );
+ checkCollapsible(subquery); // mark seenSubqueriesToExpand
+ insertLeaf(tree, subquery, t);
+}
+
+function getOrInsertSubtree<T extends TestQuery>(
+ key: string,
+ parent: TestSubtree,
+ createSubtree: () => Omit<TestSubtree<T>, 'children'>
+): TestSubtree<T> {
+ let v: TestSubtree<T>;
+ const child = parent.children.get(key);
+ if (child !== undefined) {
+ assert('children' in child); // Make sure cached subtree is not actually a leaf
+ v = child as TestSubtree<T>;
+ } else {
+ v = { ...createSubtree(), children: new Map() };
+ parent.children.set(key, v);
+ }
+ return v;
+}
+
+function insertLeaf(parent: TestSubtree, query: TestQuerySingleCase, t: RunCase) {
+ const leaf: TestTreeLeaf = {
+ readableRelativeName: readableNameForCase(query),
+ query,
+ run: (rec, expectations) => t.run(rec, query, expectations || []),
+ isUnimplemented: t.isUnimplemented,
+ };
+
+ // This is a leaf (e.g. s:f:t:x=1;* -> s:f:t:x=1). The key is always ''.
+ const key = '';
+ assert(!parent.children.has(key), `Duplicate testcase: ${query}`);
+ parent.children.set(key, leaf);
+}
+
+function dissolveSingleChildTrees(tree: TestTreeNode): TestTreeNode {
+ if ('children' in tree) {
+ const shouldDissolveThisTree =
+ tree.children.size === 1 && tree.query.depthInLevel !== 0 && tree.description === undefined;
+ if (shouldDissolveThisTree) {
+ // Loops exactly once
+ for (const [, child] of tree.children) {
+ // Recurse on child
+ return dissolveSingleChildTrees(child);
+ }
+ }
+
+ for (const [k, child] of tree.children) {
+ // Recurse on each child
+ const newChild = dissolveSingleChildTrees(child);
+ if (newChild !== child) {
+ tree.children.set(k, newChild);
+ }
+ }
+ }
+ return tree;
+}
+
+/** Generate a readable relative name for a case (used in standalone). */
+function readableNameForCase(query: TestQuerySingleCase): string {
+ const paramsKeys = Object.keys(query.params);
+ if (paramsKeys.length === 0) {
+ return query.testPathParts[query.testPathParts.length - 1] + kBigSeparator;
+ } else {
+ const lastKey = paramsKeys[paramsKeys.length - 1];
+ return stringifySingleParam(lastKey, query.params[lastKey]);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/util.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/util.ts
new file mode 100644
index 0000000000..37a5db3568
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/util.ts
@@ -0,0 +1,10 @@
+/**
+ * Error without a stack, which can be used to fatally exit from `tool/` scripts with a
+ * user-friendly message (and no confusing stack).
+ */
+export class StacklessError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.stack = undefined;
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/version.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/version.ts
new file mode 100644
index 0000000000..53cc97482e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/internal/version.ts
@@ -0,0 +1 @@
+export const version = 'unknown';
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts
new file mode 100644
index 0000000000..463546c06d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts
@@ -0,0 +1,278 @@
+/* eslint no-console: "off" */
+
+import * as fs from 'fs';
+
+import { dataCache } from '../framework/data_cache.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { prettyPrintLog } from '../internal/logging/log_message.js';
+import { Logger } from '../internal/logging/logger.js';
+import { LiveTestCaseResult } from '../internal/logging/result.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { parseExpectationsForTestQuery } from '../internal/query/query.js';
+import { Colors } from '../util/colors.js';
+import { setGPUProvider } from '../util/navigator_gpu.js';
+import { assert, unreachable } from '../util/util.js';
+
+import sys from './helper/sys.js';
+
+function usage(rc: number): never {
+ console.log(`Usage:
+ tools/run_${sys.type} [OPTIONS...] QUERIES...
+ tools/run_${sys.type} 'unittests:*' 'webgpu:buffers,*'
+Options:
+ --colors Enable ANSI colors in output.
+ --coverage Emit coverage data.
+ --verbose Print result/log of every test as it runs.
+ --list Print all testcase names that match the given query and exit.
+ --debug Include debug messages in logging.
+ --print-json Print the complete result JSON in the output.
+ --expectations Path to expectations file.
+ --gpu-provider Path to node module that provides the GPU implementation.
+ --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value>
+ --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests
+ --quiet Suppress summary information in output
+`);
+ return sys.exit(rc);
+}
+
+// The interface that exposes creation of the GPU, and optional interface to code coverage.
+interface GPUProviderModule {
+ // @returns a GPU with the given flags
+ create(flags: string[]): GPU;
+ // An optional interface to a CodeCoverageProvider
+ coverage?: CodeCoverageProvider;
+}
+
+interface CodeCoverageProvider {
+ // Starts collecting code coverage
+ begin(): void;
+ // Ends collecting of code coverage, returning the coverage data.
+ // This data is opaque (implementation defined).
+ end(): string;
+}
+
+type listModes = 'none' | 'cases' | 'unimplemented';
+
+Colors.enabled = false;
+
+let verbose = false;
+let emitCoverage = false;
+let listMode: listModes = 'none';
+let debug = false;
+let printJSON = false;
+let quiet = false;
+let loadWebGPUExpectations: Promise<unknown> | undefined = undefined;
+let gpuProviderModule: GPUProviderModule | undefined = undefined;
+let dataPath: string | undefined = undefined;
+
+const queries: string[] = [];
+const gpuProviderFlags: string[] = [];
+for (let i = 0; i < sys.args.length; ++i) {
+ const a = sys.args[i];
+ if (a.startsWith('-')) {
+ if (a === '--colors') {
+ Colors.enabled = true;
+ } else if (a === '--coverage') {
+ emitCoverage = true;
+ } else if (a === '--verbose') {
+ verbose = true;
+ } else if (a === '--list') {
+ listMode = 'cases';
+ } else if (a === '--list-unimplemented') {
+ listMode = 'unimplemented';
+ } else if (a === '--debug') {
+ debug = true;
+ } else if (a === '--data') {
+ dataPath = sys.args[++i];
+ } else if (a === '--print-json') {
+ printJSON = true;
+ } else if (a === '--expectations') {
+ const expectationsFile = new URL(sys.args[++i], `file://${sys.cwd()}`).pathname;
+ loadWebGPUExpectations = import(expectationsFile).then(m => m.expectations);
+ } else if (a === '--gpu-provider') {
+ const modulePath = sys.args[++i];
+ gpuProviderModule = require(modulePath);
+ } else if (a === '--gpu-provider-flag') {
+ gpuProviderFlags.push(sys.args[++i]);
+ } else if (a === '--quiet') {
+ quiet = true;
+ } else if (a === '--unroll-const-eval-loops') {
+ globalTestConfig.unrollConstEvalLoops = true;
+ } else {
+ console.log('unrecognized flag: ', a);
+ usage(1);
+ }
+ } else {
+ queries.push(a);
+ }
+}
+
+let codeCoverage: CodeCoverageProvider | undefined = undefined;
+
+if (gpuProviderModule) {
+ setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags));
+ if (emitCoverage) {
+ codeCoverage = gpuProviderModule.coverage;
+ if (codeCoverage === undefined) {
+ console.error(
+ `--coverage specified, but the GPUProviderModule does not support code coverage.
+Did you remember to build with code coverage instrumentation enabled?`
+ );
+ sys.exit(1);
+ }
+ }
+}
+
+if (dataPath !== undefined) {
+ dataCache.setStore({
+ load: (path: string) => {
+ return new Promise<string>((resolve, reject) => {
+ fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => {
+ if (err !== null) {
+ reject(err.message);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ },
+ });
+}
+if (verbose) {
+ dataCache.setDebugLogger(console.log);
+}
+
+if (queries.length === 0) {
+ console.log('no queries specified');
+ usage(0);
+}
+
+(async () => {
+ const loader = new DefaultTestFileLoader();
+ assert(queries.length === 1, 'currently, there must be exactly one query on the cmd line');
+ const filterQuery = parseQuery(queries[0]);
+ const testcases = await loader.loadCases(filterQuery);
+ const expectations = parseExpectationsForTestQuery(
+ await (loadWebGPUExpectations ?? []),
+ filterQuery
+ );
+
+ Logger.globalDebugMode = debug;
+ const log = new Logger();
+
+ const failed: Array<[string, LiveTestCaseResult]> = [];
+ const warned: Array<[string, LiveTestCaseResult]> = [];
+ const skipped: Array<[string, LiveTestCaseResult]> = [];
+
+ let total = 0;
+
+ if (codeCoverage !== undefined) {
+ codeCoverage.begin();
+ }
+
+ for (const testcase of testcases) {
+ const name = testcase.query.toString();
+ switch (listMode) {
+ case 'cases':
+ console.log(name);
+ continue;
+ case 'unimplemented':
+ if (testcase.isUnimplemented) {
+ console.log(name);
+ }
+ continue;
+ default:
+ break;
+ }
+
+ const [rec, res] = log.record(name);
+ await testcase.run(rec, expectations);
+
+ if (verbose) {
+ printResults([[name, res]]);
+ }
+
+ total++;
+ switch (res.status) {
+ case 'pass':
+ break;
+ case 'fail':
+ failed.push([name, res]);
+ break;
+ case 'warn':
+ warned.push([name, res]);
+ break;
+ case 'skip':
+ skipped.push([name, res]);
+ break;
+ default:
+ unreachable('unrecognized status');
+ }
+ }
+
+ if (codeCoverage !== undefined) {
+ const coverage = codeCoverage.end();
+ console.log(`Code-coverage: [[${coverage}]]`);
+ }
+
+ if (listMode !== 'none') {
+ return;
+ }
+
+ assert(total > 0, 'found no tests!');
+
+ // MAINTENANCE_TODO: write results out somewhere (a file?)
+ if (printJSON) {
+ console.log(log.asJSON(2));
+ }
+
+ if (!quiet) {
+ if (skipped.length) {
+ console.log('');
+ console.log('** Skipped **');
+ printResults(skipped);
+ }
+ if (warned.length) {
+ console.log('');
+ console.log('** Warnings **');
+ printResults(warned);
+ }
+ if (failed.length) {
+ console.log('');
+ console.log('** Failures **');
+ printResults(failed);
+ }
+
+ const passed = total - warned.length - failed.length - skipped.length;
+ const pct = (x: number) => ((100 * x) / total).toFixed(2);
+ const rpt = (x: number) => {
+ const xs = x.toString().padStart(1 + Math.log10(total), ' ');
+ return `${xs} / ${total} = ${pct(x).padStart(6, ' ')}%`;
+ };
+ console.log('');
+ console.log(`** Summary **
+Passed w/o warnings = ${rpt(passed)}
+Passed with warnings = ${rpt(warned.length)}
+Skipped = ${rpt(skipped.length)}
+Failed = ${rpt(failed.length)}`);
+ }
+
+ if (failed.length || warned.length) {
+ sys.exit(1);
+ }
+})().catch(ex => {
+ console.log(ex.stack ?? ex.toString());
+ sys.exit(1);
+});
+
+function printResults(results: Array<[string, LiveTestCaseResult]>): void {
+ for (const [name, r] of results) {
+ console.log(`[${r.status}] ${name} (${r.timems}ms). Log:`);
+ if (r.logs) {
+ for (const l of r.logs) {
+ console.log(prettyPrintLog(l));
+ }
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts
new file mode 100644
index 0000000000..bec14694a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts
@@ -0,0 +1,22 @@
+let windowURL: URL | undefined = undefined;
+function getWindowURL() {
+ if (windowURL === undefined) {
+ windowURL = new URL(window.location.toString());
+ }
+ return windowURL;
+}
+
+export function optionEnabled(
+ opt: string,
+ searchParams: URLSearchParams = getWindowURL().searchParams
+): boolean {
+ const val = searchParams.get(opt);
+ return val !== null && val !== '0';
+}
+
+export function optionString(
+ opt: string,
+ searchParams: URLSearchParams = getWindowURL().searchParams
+): string {
+ return searchParams.get(opt) || '';
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/sys.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/sys.ts
new file mode 100644
index 0000000000..d2e07ff26d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/sys.ts
@@ -0,0 +1,46 @@
+/* eslint no-process-exit: "off" */
+/* eslint @typescript-eslint/no-namespace: "off" */
+
+function node() {
+ const { existsSync } = require('fs');
+
+ return {
+ type: 'node',
+ existsSync,
+ args: process.argv.slice(2),
+ cwd: () => process.cwd(),
+ exit: (code?: number | undefined) => process.exit(code),
+ };
+}
+
+declare global {
+ namespace Deno {
+ function readFileSync(path: string): Uint8Array;
+ const args: string[];
+ const cwd: () => string;
+ function exit(code?: number): never;
+ }
+}
+
+function deno() {
+ function existsSync(path: string) {
+ try {
+ Deno.readFileSync(path);
+ return true;
+ } catch (err) {
+ return false;
+ }
+ }
+
+ return {
+ type: 'deno',
+ existsSync,
+ args: Deno.args,
+ cwd: Deno.cwd,
+ exit: Deno.exit,
+ };
+}
+
+const sys = typeof globalThis.process !== 'undefined' ? node() : deno();
+
+export default sys;
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts
new file mode 100644
index 0000000000..9af555f36d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts
@@ -0,0 +1,32 @@
+import { setBaseResourcePath } from '../../framework/resources.js';
+import { DefaultTestFileLoader } from '../../internal/file_loader.js';
+import { Logger } from '../../internal/logging/logger.js';
+import { parseQuery } from '../../internal/query/parseQuery.js';
+import { TestQueryWithExpectation } from '../../internal/query/query.js';
+import { assert } from '../../util/util.js';
+
+// Should be DedicatedWorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+declare const self: any;
+
+const loader = new DefaultTestFileLoader();
+
+setBaseResourcePath('../../../resources');
+
+self.onmessage = async (ev: MessageEvent) => {
+ const query: string = ev.data.query;
+ const expectations: TestQueryWithExpectation[] = ev.data.expectations;
+ const debug: boolean = ev.data.debug;
+
+ Logger.globalDebugMode = debug;
+ const log = new Logger();
+
+ const testcases = Array.from(await loader.loadCases(parseQuery(query)));
+ assert(testcases.length === 1, 'worker query resulted in != 1 cases');
+
+ const testcase = testcases[0];
+ const [rec, result] = log.record(testcase.query.toString());
+ await testcase.run(rec, expectations);
+
+ self.postMessage({ query, result });
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts
new file mode 100644
index 0000000000..2ddc3a951b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts
@@ -0,0 +1,44 @@
+import { LogMessageWithStack } from '../../internal/logging/log_message.js';
+import { TransferredTestCaseResult, LiveTestCaseResult } from '../../internal/logging/result.js';
+import { TestCaseRecorder } from '../../internal/logging/test_case_recorder.js';
+import { TestQueryWithExpectation } from '../../internal/query/query.js';
+
+export class TestWorker {
+ private readonly debug: boolean;
+ private readonly worker: Worker;
+ private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();
+
+ constructor(debug: boolean) {
+ this.debug = debug;
+
+ const selfPath = import.meta.url;
+ const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
+ const workerPath = selfPathDir + '/test_worker-worker.js';
+ this.worker = new Worker(workerPath, { type: 'module' });
+ this.worker.onmessage = ev => {
+ const query: string = ev.data.query;
+ const result: TransferredTestCaseResult = ev.data.result;
+ if (result.logs) {
+ for (const l of result.logs) {
+ Object.setPrototypeOf(l, LogMessageWithStack.prototype);
+ }
+ }
+ this.resolvers.get(query)!(result as LiveTestCaseResult);
+
+ // MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
+ // update the entire results JSON somehow at some point).
+ };
+ }
+
+ async run(
+ rec: TestCaseRecorder,
+ query: string,
+ expectations: TestQueryWithExpectation[] = []
+ ): Promise<void> {
+ this.worker.postMessage({ query, expectations, debug: this.debug });
+ const workerResult = await new Promise<LiveTestCaseResult>(resolve => {
+ this.resolvers.set(query, resolve);
+ });
+ rec.injectResult(workerResult);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts
new file mode 100644
index 0000000000..350a864a34
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts
@@ -0,0 +1,227 @@
+/* eslint no-console: "off" */
+
+import * as fs from 'fs';
+import * as http from 'http';
+import { AddressInfo } from 'net';
+
+import { dataCache } from '../framework/data_cache.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { prettyPrintLog } from '../internal/logging/log_message.js';
+import { Logger } from '../internal/logging/logger.js';
+import { LiveTestCaseResult, Status } from '../internal/logging/result.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { TestQueryWithExpectation } from '../internal/query/query.js';
+import { TestTreeLeaf } from '../internal/tree.js';
+import { Colors } from '../util/colors.js';
+import { setGPUProvider } from '../util/navigator_gpu.js';
+
+import sys from './helper/sys.js';
+
+function usage(rc: number): never {
+ console.log(`Usage:
+ tools/run_${sys.type} [OPTIONS...]
+Options:
+ --colors Enable ANSI colors in output.
+ --coverage Add coverage data to each result.
+ --data Path to the data cache directory.
+ --verbose Print result/log of every test as it runs.
+ --gpu-provider Path to node module that provides the GPU implementation.
+ --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value>
+ --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests
+ --u Flag to set on the gpu-provider as <flag>=<value>
+
+Provides an HTTP server used for running tests via an HTTP RPC interface
+To run a test, perform an HTTP GET or POST at the URL:
+ http://localhost:port/run?<test-name>
+To shutdown the server perform an HTTP GET or POST at the URL:
+ http://localhost:port/terminate
+`);
+ return sys.exit(rc);
+}
+
+interface RunResult {
+ // The result of the test
+ status: Status;
+ // Any additional messages printed
+ message: string;
+ // Code coverage data, if the server was started with `--coverage`
+ // This data is opaque (implementation defined).
+ coverageData?: string;
+}
+
+// The interface that exposes creation of the GPU, and optional interface to code coverage.
+interface GPUProviderModule {
+ // @returns a GPU with the given flags
+ create(flags: string[]): GPU;
+ // An optional interface to a CodeCoverageProvider
+ coverage?: CodeCoverageProvider;
+}
+
+interface CodeCoverageProvider {
+ // Starts collecting code coverage
+ begin(): void;
+ // Ends collecting of code coverage, returning the coverage data.
+ // This data is opaque (implementation defined).
+ end(): string;
+}
+
+if (!sys.existsSync('src/common/runtime/cmdline.ts')) {
+ console.log('Must be run from repository root');
+ usage(1);
+}
+
+Colors.enabled = false;
+
+let emitCoverage = false;
+let verbose = false;
+let gpuProviderModule: GPUProviderModule | undefined = undefined;
+let dataPath: string | undefined = undefined;
+
+const gpuProviderFlags: string[] = [];
+for (let i = 0; i < sys.args.length; ++i) {
+ const a = sys.args[i];
+ if (a.startsWith('-')) {
+ if (a === '--colors') {
+ Colors.enabled = true;
+ } else if (a === '--coverage') {
+ emitCoverage = true;
+ } else if (a === '--data') {
+ dataPath = sys.args[++i];
+ } else if (a === '--gpu-provider') {
+ const modulePath = sys.args[++i];
+ gpuProviderModule = require(modulePath);
+ } else if (a === '--gpu-provider-flag') {
+ gpuProviderFlags.push(sys.args[++i]);
+ } else if (a === '--unroll-const-eval-loops') {
+ globalTestConfig.unrollConstEvalLoops = true;
+ } else if (a === '--help') {
+ usage(1);
+ } else if (a === '--verbose') {
+ verbose = true;
+ } else {
+ console.log(`unrecognised flag: ${a}`);
+ }
+ }
+}
+
+let codeCoverage: CodeCoverageProvider | undefined = undefined;
+
+if (gpuProviderModule) {
+ setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags));
+
+ if (emitCoverage) {
+ codeCoverage = gpuProviderModule.coverage;
+ if (codeCoverage === undefined) {
+ console.error(
+ `--coverage specified, but the GPUProviderModule does not support code coverage.
+Did you remember to build with code coverage instrumentation enabled?`
+ );
+ sys.exit(1);
+ }
+ }
+}
+
+if (dataPath !== undefined) {
+ dataCache.setStore({
+ load: (path: string) => {
+ return new Promise<string>((resolve, reject) => {
+ fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => {
+ if (err !== null) {
+ reject(err.message);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ },
+ });
+}
+if (verbose) {
+ dataCache.setDebugLogger(console.log);
+}
+
+(async () => {
+ Logger.globalDebugMode = verbose;
+ const log = new Logger();
+ const testcases = new Map<string, TestTreeLeaf>();
+
+ async function runTestcase(
+ testcase: TestTreeLeaf,
+ expectations: TestQueryWithExpectation[] = []
+ ): Promise<LiveTestCaseResult> {
+ const name = testcase.query.toString();
+ const [rec, res] = log.record(name);
+ await testcase.run(rec, expectations);
+ return res;
+ }
+
+ const server = http.createServer(
+ async (request: http.IncomingMessage, response: http.ServerResponse) => {
+ if (request.url === undefined) {
+ response.end('invalid url');
+ return;
+ }
+
+ const loadCasesPrefix = '/load?';
+ const runPrefix = '/run?';
+ const terminatePrefix = '/terminate';
+
+ if (request.url.startsWith(loadCasesPrefix)) {
+ const query = request.url.substr(loadCasesPrefix.length);
+ try {
+ const webgpuQuery = parseQuery(query);
+ const loader = new DefaultTestFileLoader();
+ for (const testcase of await loader.loadCases(webgpuQuery)) {
+ testcases.set(testcase.query.toString(), testcase);
+ }
+ response.statusCode = 200;
+ response.end();
+ } catch (err) {
+ response.statusCode = 500;
+ response.end(`load failed with error: ${err}\n${(err as Error).stack}`);
+ }
+ } else if (request.url.startsWith(runPrefix)) {
+ const name = request.url.substr(runPrefix.length);
+ try {
+ const testcase = testcases.get(name);
+ if (testcase) {
+ if (codeCoverage !== undefined) {
+ codeCoverage.begin();
+ }
+ const result = await runTestcase(testcase);
+ const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined;
+ let message = '';
+ if (result.logs !== undefined) {
+ message = result.logs.map(log => prettyPrintLog(log)).join('\n');
+ }
+ const status = result.status;
+ const res: RunResult = { status, message, coverageData };
+ response.statusCode = 200;
+ response.end(JSON.stringify(res));
+ } else {
+ response.statusCode = 404;
+ response.end(`test case '${name}' not found`);
+ }
+ } catch (err) {
+ response.statusCode = 500;
+ response.end(`run failed with error: ${err}`);
+ }
+ } else if (request.url.startsWith(terminatePrefix)) {
+ server.close();
+ sys.exit(1);
+ } else {
+ response.statusCode = 404;
+ response.end('unhandled url request');
+ }
+ }
+ );
+
+ server.listen(0, () => {
+ const address = server.address() as AddressInfo;
+ console.log(`Server listening at [[${address.port}]]`);
+ });
+})().catch(ex => {
+ console.error(ex.stack ?? ex.toString());
+ sys.exit(1);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts
new file mode 100644
index 0000000000..0dd158fd68
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts
@@ -0,0 +1,625 @@
+// Implements the standalone test runner (see also: /standalone/index.html).
+
+import { dataCache } from '../framework/data_cache.js';
+import { setBaseResourcePath } from '../framework/resources.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { Logger } from '../internal/logging/logger.js';
+import { LiveTestCaseResult } from '../internal/logging/result.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { TestQueryLevel } from '../internal/query/query.js';
+import { TestTreeNode, TestSubtree, TestTreeLeaf, TestTree } from '../internal/tree.js';
+import { setDefaultRequestAdapterOptions } from '../util/navigator_gpu.js';
+import { assert, ErrorWithExtra, unreachable } from '../util/util.js';
+
+import { optionEnabled, optionString } from './helper/options.js';
+import { TestWorker } from './helper/test_worker.js';
+
+window.onbeforeunload = () => {
+ // Prompt user before reloading if there are any results
+ return haveSomeResults ? false : undefined;
+};
+
+let haveSomeResults = false;
+
+// The possible options for the tests.
+interface StandaloneOptions {
+ runnow: boolean;
+ worker: boolean;
+ debug: boolean;
+ unrollConstEvalLoops: boolean;
+ powerPreference: string;
+}
+
+// Extra per option info.
+interface StandaloneOptionInfo {
+ description: string;
+ parser?: (key: string) => boolean | string;
+ selectValueDescriptions?: { value: string; description: string }[];
+}
+
+// Type for info for every option. This definition means adding an option
+// will generate a compile time error if not extra info is provided.
+type StandaloneOptionsInfos = Record<keyof StandaloneOptions, StandaloneOptionInfo>;
+
+const optionsInfo: StandaloneOptionsInfos = {
+ runnow: { description: 'run immediately on load' },
+ worker: { description: 'run in a worker' },
+ debug: { description: 'show more info' },
+ unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' },
+ powerPreference: {
+ description: 'set default powerPreference for some tests',
+ parser: optionString,
+ selectValueDescriptions: [
+ { value: '', description: 'default' },
+ { value: 'low-power', description: 'low-power' },
+ { value: 'high-performance', description: 'high-performance' },
+ ],
+ },
+};
+
+/**
+ * Converts camel case to snake case.
+ * Examples:
+ * fooBar -> foo_bar
+ * parseHTMLFile -> parse_html_file
+ */
+function camelCaseToSnakeCase(id: string) {
+ return id
+ .replace(/(.)([A-Z][a-z]+)/g, '$1_$2')
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
+ .toLowerCase();
+}
+
+/**
+ * Creates a StandaloneOptions from the current URL search parameters.
+ */
+function getOptionsInfoFromSearchParameters(
+ optionsInfos: StandaloneOptionsInfos
+): StandaloneOptions {
+ const optionValues: Record<string, boolean | string> = {};
+ for (const [optionName, info] of Object.entries(optionsInfos)) {
+ const parser = info.parser || optionEnabled;
+ optionValues[optionName] = parser(camelCaseToSnakeCase(optionName));
+ }
+ return (optionValues as unknown) as StandaloneOptions;
+}
+
+// This is just a cast in one place.
+function optionsToRecord(options: StandaloneOptions) {
+ return (options as unknown) as Record<string, boolean | string>;
+}
+
+const options = getOptionsInfoFromSearchParameters(optionsInfo);
+const { runnow, debug, unrollConstEvalLoops, powerPreference } = options;
+globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
+
+Logger.globalDebugMode = debug;
+const logger = new Logger();
+
+setBaseResourcePath('../out/resources');
+
+const worker = options.worker ? new TestWorker(debug) : undefined;
+
+const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement;
+const resultsVis = document.getElementById('resultsVis')!;
+const progressElem = document.getElementById('progress')!;
+const progressTestNameElem = progressElem.querySelector('.progress-test-name')!;
+const stopButtonElem = progressElem.querySelector('button')!;
+let runDepth = 0;
+let stopRequested = false;
+
+stopButtonElem.addEventListener('click', () => {
+ stopRequested = true;
+});
+
+if (powerPreference) {
+ setDefaultRequestAdapterOptions({ powerPreference: powerPreference as GPUPowerPreference });
+}
+
+dataCache.setStore({
+ load: async (path: string) => {
+ const response = await fetch(`data/${path}`);
+ if (!response.ok) {
+ return Promise.reject(response.statusText);
+ }
+ return await response.text();
+ },
+});
+
+interface SubtreeResult {
+ pass: number;
+ fail: number;
+ warn: number;
+ skip: number;
+ total: number;
+ timems: number;
+}
+
+function emptySubtreeResult() {
+ return { pass: 0, fail: 0, warn: 0, skip: 0, total: 0, timems: 0 };
+}
+
+function mergeSubtreeResults(...results: SubtreeResult[]) {
+ const target = emptySubtreeResult();
+ for (const result of results) {
+ target.pass += result.pass;
+ target.fail += result.fail;
+ target.warn += result.warn;
+ target.skip += result.skip;
+ target.total += result.total;
+ target.timems += result.timems;
+ }
+ return target;
+}
+
+type SetCheckedRecursively = () => void;
+type GenerateSubtreeHTML = (parent: HTMLElement) => SetCheckedRecursively;
+type RunSubtree = () => Promise<SubtreeResult>;
+
+interface VisualizedSubtree {
+ generateSubtreeHTML: GenerateSubtreeHTML;
+ runSubtree: RunSubtree;
+}
+
+// DOM generation
+
+function memoize<T>(fn: () => T): () => T {
+ let value: T | undefined;
+ return () => {
+ if (value === undefined) {
+ value = fn();
+ }
+ return value;
+ };
+}
+
+function makeTreeNodeHTML(tree: TestTreeNode, parentLevel: TestQueryLevel): VisualizedSubtree {
+ let subtree: VisualizedSubtree;
+
+ if ('children' in tree) {
+ subtree = makeSubtreeHTML(tree, parentLevel);
+ } else {
+ subtree = makeCaseHTML(tree);
+ }
+
+ const generateMyHTML = (parentElement: HTMLElement) => {
+ const div = $('<div>').appendTo(parentElement)[0];
+ return subtree.generateSubtreeHTML(div);
+ };
+ return { runSubtree: subtree.runSubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
+ // Becomes set once the case has been run once.
+ let caseResult: LiveTestCaseResult | undefined;
+
+ // Becomes set once the DOM for this case exists.
+ let clearRenderedResult: (() => void) | undefined;
+ let updateRenderedResult: (() => void) | undefined;
+
+ const name = t.query.toString();
+ const runSubtree = async () => {
+ if (clearRenderedResult) clearRenderedResult();
+
+ const result: SubtreeResult = emptySubtreeResult();
+ progressTestNameElem.textContent = name;
+
+ haveSomeResults = true;
+ const [rec, res] = logger.record(name);
+ caseResult = res;
+ if (worker) {
+ await worker.run(rec, name);
+ } else {
+ await t.run(rec);
+ }
+
+ result.total++;
+ result.timems += caseResult.timems;
+ switch (caseResult.status) {
+ case 'pass':
+ result.pass++;
+ break;
+ case 'fail':
+ result.fail++;
+ break;
+ case 'skip':
+ result.skip++;
+ break;
+ case 'warn':
+ result.warn++;
+ break;
+ default:
+ unreachable();
+ }
+
+ if (updateRenderedResult) updateRenderedResult();
+
+ return result;
+ };
+
+ const generateSubtreeHTML = (div: HTMLElement) => {
+ div.classList.add('testcase');
+
+ const caselogs = $('<div>').addClass('testcaselogs').hide();
+ const [casehead, setChecked] = makeTreeNodeHeaderHTML(t, runSubtree, 2, checked => {
+ checked ? caselogs.show() : caselogs.hide();
+ });
+ const casetime = $('<div>').addClass('testcasetime').html('ms').appendTo(casehead);
+ div.appendChild(casehead);
+ div.appendChild(caselogs[0]);
+
+ clearRenderedResult = () => {
+ div.removeAttribute('data-status');
+ casetime.text('ms');
+ caselogs.empty();
+ };
+
+ updateRenderedResult = () => {
+ if (caseResult) {
+ div.setAttribute('data-status', caseResult.status);
+
+ casetime.text(caseResult.timems.toFixed(4) + ' ms');
+
+ if (caseResult.logs) {
+ caselogs.empty();
+ for (const l of caseResult.logs) {
+ const caselog = $('<div>').addClass('testcaselog').appendTo(caselogs);
+ $('<button>')
+ .addClass('testcaselogbtn')
+ .attr('alt', 'Log stack to console')
+ .attr('title', 'Log stack to console')
+ .appendTo(caselog)
+ .on('click', () => {
+ consoleLogError(l);
+ });
+ $('<pre>').addClass('testcaselogtext').appendTo(caselog).text(l.toJSON());
+ }
+ }
+ }
+ };
+
+ updateRenderedResult();
+
+ return setChecked;
+ };
+
+ return { runSubtree, generateSubtreeHTML };
+}
+
+function makeSubtreeHTML(n: TestSubtree, parentLevel: TestQueryLevel): VisualizedSubtree {
+ let subtreeResult: SubtreeResult = emptySubtreeResult();
+ // Becomes set once the DOM for this case exists.
+ let clearRenderedResult: (() => void) | undefined;
+ let updateRenderedResult: (() => void) | undefined;
+
+ const { runSubtree, generateSubtreeHTML } = makeSubtreeChildrenHTML(
+ n.children.values(),
+ n.query.level
+ );
+
+ const runMySubtree = async () => {
+ if (runDepth === 0) {
+ stopRequested = false;
+ progressElem.style.display = '';
+ }
+ if (stopRequested) {
+ const result = emptySubtreeResult();
+ result.skip = 1;
+ result.total = 1;
+ return result;
+ }
+
+ ++runDepth;
+
+ if (clearRenderedResult) clearRenderedResult();
+ subtreeResult = await runSubtree();
+ if (updateRenderedResult) updateRenderedResult();
+
+ --runDepth;
+ if (runDepth === 0) {
+ progressElem.style.display = 'none';
+ }
+
+ return subtreeResult;
+ };
+
+ const generateMyHTML = (div: HTMLElement) => {
+ const subtreeHTML = $('<div>').addClass('subtreechildren');
+ const generateSubtree = memoize(() => generateSubtreeHTML(subtreeHTML[0]));
+
+ // Hide subtree - it's not generated yet.
+ subtreeHTML.hide();
+ const [header, setChecked] = makeTreeNodeHeaderHTML(n, runMySubtree, parentLevel, checked => {
+ if (checked) {
+ // Make sure the subtree is generated and then show it.
+ generateSubtree();
+ subtreeHTML.show();
+ } else {
+ subtreeHTML.hide();
+ }
+ });
+
+ div.classList.add('subtree');
+ div.classList.add(['', 'multifile', 'multitest', 'multicase'][n.query.level]);
+ div.appendChild(header);
+ div.appendChild(subtreeHTML[0]);
+
+ clearRenderedResult = () => {
+ div.removeAttribute('data-status');
+ };
+
+ updateRenderedResult = () => {
+ let status = '';
+ if (subtreeResult.pass > 0) {
+ status += 'pass';
+ }
+ if (subtreeResult.fail > 0) {
+ status += 'fail';
+ }
+ div.setAttribute('data-status', status);
+ if (autoCloseOnPass.checked && status === 'pass') {
+ div.firstElementChild!.removeAttribute('open');
+ }
+ };
+
+ updateRenderedResult();
+
+ return () => {
+ setChecked();
+ const setChildrenChecked = generateSubtree();
+ setChildrenChecked();
+ };
+ };
+
+ return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function makeSubtreeChildrenHTML(
+ children: Iterable<TestTreeNode>,
+ parentLevel: TestQueryLevel
+): VisualizedSubtree {
+ const childFns = Array.from(children, subtree => makeTreeNodeHTML(subtree, parentLevel));
+
+ const runMySubtree = async () => {
+ const results: SubtreeResult[] = [];
+ for (const { runSubtree } of childFns) {
+ results.push(await runSubtree());
+ }
+ return mergeSubtreeResults(...results);
+ };
+ const generateMyHTML = (div: HTMLElement) => {
+ const setChildrenChecked = Array.from(childFns, ({ generateSubtreeHTML }) =>
+ generateSubtreeHTML(div)
+ );
+
+ return () => {
+ for (const setChildChecked of setChildrenChecked) {
+ setChildChecked();
+ }
+ };
+ };
+
+ return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function consoleLogError(e: Error | ErrorWithExtra | undefined) {
+ if (e === undefined) return;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ (globalThis as any)._stack = e;
+ /* eslint-disable-next-line no-console */
+ console.log('_stack =', e);
+ if ('extra' in e && e.extra !== undefined) {
+ /* eslint-disable-next-line no-console */
+ console.log('_stack.extra =', e.extra);
+ }
+}
+
+function makeTreeNodeHeaderHTML(
+ n: TestTreeNode,
+ runSubtree: RunSubtree,
+ parentLevel: TestQueryLevel,
+ onChange: (checked: boolean) => void
+): [HTMLElement, SetCheckedRecursively] {
+ const isLeaf = 'run' in n;
+ const div = $('<details>').addClass('nodeheader');
+ const header = $('<summary>').appendTo(div);
+
+ const setChecked = () => {
+ div.prop('open', true); // (does not fire onChange)
+ onChange(true);
+ };
+
+ const href = `?${worker ? 'worker&' : ''}${debug ? 'debug&' : ''}q=${n.query.toString()}`;
+ if (onChange) {
+ div.on('toggle', function (this) {
+ onChange((this as HTMLDetailsElement).open);
+ });
+
+ // Expand the shallower parts of the tree at load.
+ // Also expand completely within subtrees that are at the same query level
+ // (e.g. s:f:t,* and s:f:t,t,*).
+ if (n.query.level <= lastQueryLevelToExpand || n.query.level === parentLevel) {
+ setChecked();
+ }
+ }
+ const runtext = isLeaf ? 'Run case' : 'Run subtree';
+ $('<button>')
+ .addClass(isLeaf ? 'leafrun' : 'subtreerun')
+ .attr('alt', runtext)
+ .attr('title', runtext)
+ .on('click', () => void runSubtree())
+ .appendTo(header);
+ $('<a>')
+ .addClass('nodelink')
+ .attr('href', href)
+ .attr('alt', 'Open')
+ .attr('title', 'Open')
+ .appendTo(header);
+ if ('testCreationStack' in n && n.testCreationStack) {
+ $('<button>')
+ .addClass('testcaselogbtn')
+ .attr('alt', 'Log test creation stack to console')
+ .attr('title', 'Log test creation stack to console')
+ .appendTo(header)
+ .on('click', () => {
+ consoleLogError(n.testCreationStack);
+ });
+ }
+ const nodetitle = $('<div>').addClass('nodetitle').appendTo(header);
+ const nodecolumns = $('<span>').addClass('nodecolumns').appendTo(nodetitle);
+ {
+ $('<input>')
+ .attr('type', 'text')
+ .prop('readonly', true)
+ .addClass('nodequery')
+ .val(n.query.toString())
+ .appendTo(nodecolumns);
+ if (n.subtreeCounts) {
+ $('<span>')
+ .attr('title', '(Nodes with TODOs) / (Total test count)')
+ .text(TestTree.countsToString(n))
+ .appendTo(nodecolumns);
+ }
+ }
+ if ('description' in n && n.description) {
+ nodetitle.append('&nbsp;');
+ $('<pre>') //
+ .addClass('nodedescription')
+ .text(n.description)
+ .appendTo(header);
+ }
+ return [div[0], setChecked];
+}
+
+// Collapse s:f:t:* or s:f:t:c by default.
+let lastQueryLevelToExpand: TestQueryLevel = 2;
+
+type ParamValue = string | undefined | null | boolean | string[];
+
+/**
+ * Takes an array of string, ParamValue and returns an array of pairs
+ * of [key, value] where value is a string. Converts boolean to '0' or '1'.
+ */
+function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] {
+ const key = camelCaseToSnakeCase(k);
+ if (typeof v === 'boolean') {
+ return [[key, v ? '1' : '0']];
+ } else if (Array.isArray(v)) {
+ return v.map(v => [key, v]);
+ } else {
+ return [[key, v!.toString()]];
+ }
+}
+
+/**
+ * Converts key value pairs to a search string.
+ * Keys will appear in order in the search string.
+ * Values can be undefined, null, boolean, string, or string[]
+ * If the value is falsy the key will not appear in the search string.
+ * If the value is an array the key will appear multiple times.
+ *
+ * @param params Some object with key value pairs.
+ * @returns a search string.
+ */
+function prepareParams(params: Record<string, ParamValue>): string {
+ const pairsArrays = Object.entries(params)
+ .filter(([, v]) => !!v)
+ .map(keyValueToPairs);
+ const pairs = pairsArrays.flat();
+ return new URLSearchParams(pairs).toString();
+}
+
+void (async () => {
+ const loader = new DefaultTestFileLoader();
+
+ // MAINTENANCE_TODO: start populating page before waiting for everything to load?
+ const qs = new URLSearchParams(window.location.search).getAll('q');
+ if (qs.length === 0) {
+ qs.push('webgpu:*');
+ }
+
+ // Update the URL bar to match the exact current options.
+ const updateURLWithCurrentOptions = () => {
+ const search = prepareParams(optionsToRecord(options));
+ let url = `${window.location.origin}${window.location.pathname}`;
+ // Add in q separately to avoid escaping punctuation marks.
+ url += `?${search}${search ? '&' : ''}${qs.map(q => 'q=' + q).join('&')}`;
+ window.history.replaceState(null, '', url.toString());
+ };
+ updateURLWithCurrentOptions();
+
+ const addOptionsToPage = (options: StandaloneOptions, optionsInfos: StandaloneOptionsInfos) => {
+ const optionsElem = $('table#options>tbody')[0];
+ const optionValues = optionsToRecord(options);
+
+ const createCheckbox = (optionName: string) => {
+ return $(`<input>`)
+ .attr('type', 'checkbox')
+ .prop('checked', optionValues[optionName] as boolean)
+ .on('change', function () {
+ optionValues[optionName] = (this as HTMLInputElement).checked;
+ updateURLWithCurrentOptions();
+ });
+ };
+
+ const createSelect = (optionName: string, info: StandaloneOptionInfo) => {
+ const select = $('<select>').on('change', function () {
+ optionValues[optionName] = (this as HTMLInputElement).value;
+ updateURLWithCurrentOptions();
+ });
+ const currentValue = optionValues[optionName];
+ for (const { value, description } of info.selectValueDescriptions!) {
+ $('<option>')
+ .text(description)
+ .val(value)
+ .prop('selected', value === currentValue)
+ .appendTo(select);
+ }
+ return select;
+ };
+
+ for (const [optionName, info] of Object.entries(optionsInfos)) {
+ const input =
+ typeof optionValues[optionName] === 'boolean'
+ ? createCheckbox(optionName)
+ : createSelect(optionName, info);
+ $('<tr>')
+ .append($('<td>').append(input))
+ .append($('<td>').text(camelCaseToSnakeCase(optionName)))
+ .append($('<td>').text(info.description))
+ .appendTo(optionsElem);
+ }
+ };
+ addOptionsToPage(options, optionsInfo);
+
+ assert(qs.length === 1, 'currently, there must be exactly one ?q=');
+ const rootQuery = parseQuery(qs[0]);
+ if (rootQuery.level > lastQueryLevelToExpand) {
+ lastQueryLevelToExpand = rootQuery.level;
+ }
+ loader.addEventListener('import', ev => {
+ $('#info')[0].textContent = `loading: ${ev.data.url}`;
+ });
+ loader.addEventListener('finish', () => {
+ $('#info')[0].textContent = '';
+ });
+ const tree = await loader.loadTree(rootQuery);
+
+ tree.dissolveSingleChildTrees();
+
+ const { runSubtree, generateSubtreeHTML } = makeSubtreeHTML(tree.root, 1);
+ const setTreeCheckedRecursively = generateSubtreeHTML(resultsVis);
+
+ document.getElementById('expandall')!.addEventListener('click', () => {
+ setTreeCheckedRecursively();
+ });
+
+ document.getElementById('copyResultsJSON')!.addEventListener('click', () => {
+ void navigator.clipboard.writeText(logger.asJSON(2));
+ });
+
+ if (runnow) {
+ void runSubtree();
+ }
+})();
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts
new file mode 100644
index 0000000000..2cb9f8dbf7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts
@@ -0,0 +1,83 @@
+// Implements the wpt-embedded test runner (see also: wpt/cts.https.html).
+
+import { globalTestConfig } from '../framework/test_config.js';
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { prettyPrintLog } from '../internal/logging/log_message.js';
+import { Logger } from '../internal/logging/logger.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/query/query.js';
+import { assert } from '../util/util.js';
+
+import { optionEnabled } from './helper/options.js';
+import { TestWorker } from './helper/test_worker.js';
+
+// testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html)
+declare interface WptTestObject {
+ step(f: () => void): void;
+ done(): void;
+}
+declare function setup(properties: { explicit_done?: boolean }): void;
+declare function promise_test(f: (t: WptTestObject) => Promise<void>, name: string): void;
+declare function done(): void;
+declare function assert_unreached(description: string): void;
+
+declare const loadWebGPUExpectations: Promise<unknown> | undefined;
+declare const shouldWebGPUCTSFailOnWarnings: Promise<boolean> | undefined;
+
+setup({
+ // It's convenient for us to asynchronously add tests to the page. Prevent done() from being
+ // called implicitly when the page is finished loading.
+ explicit_done: true,
+});
+
+void (async () => {
+ const workerEnabled = optionEnabled('worker');
+ const worker = workerEnabled ? new TestWorker(false) : undefined;
+
+ globalTestConfig.unrollConstEvalLoops = optionEnabled('unroll_const_eval_loops');
+
+ const failOnWarnings =
+ typeof shouldWebGPUCTSFailOnWarnings !== 'undefined' && (await shouldWebGPUCTSFailOnWarnings);
+
+ const loader = new DefaultTestFileLoader();
+ const qs = new URLSearchParams(window.location.search).getAll('q');
+ assert(qs.length === 1, 'currently, there must be exactly one ?q=');
+ const filterQuery = parseQuery(qs[0]);
+ const testcases = await loader.loadCases(filterQuery);
+
+ const expectations =
+ typeof loadWebGPUExpectations !== 'undefined'
+ ? parseExpectationsForTestQuery(
+ await loadWebGPUExpectations,
+ filterQuery,
+ new URL(window.location.href)
+ )
+ : [];
+
+ const log = new Logger();
+
+ for (const testcase of testcases) {
+ const name = testcase.query.toString();
+ // For brevity, display the case name "relative" to the ?q= path.
+ const shortName = relativeQueryString(filterQuery, testcase.query) || '(case)';
+
+ const wpt_fn = async () => {
+ const [rec, res] = log.record(name);
+ if (worker) {
+ await worker.run(rec, name, expectations);
+ } else {
+ await testcase.run(rec, expectations);
+ }
+
+ // Unfortunately, it seems not possible to surface any logs for warn/skip.
+ if (res.status === 'fail' || (res.status === 'warn' && failOnWarnings)) {
+ const logs = (res.logs ?? []).map(prettyPrintLog);
+ assert_unreached('\n' + logs.join('\n') + '\n');
+ }
+ };
+
+ promise_test(wpt_fn, shortName);
+ }
+
+ done();
+})();
diff --git a/dom/webgpu/tests/cts/checkout/src/common/templates/cts.https.html b/dom/webgpu/tests/cts/checkout/src/common/templates/cts.https.html
new file mode 100644
index 0000000000..2961f0c3ee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/templates/cts.https.html
@@ -0,0 +1,32 @@
+<!--
+ This test suite is built from the TypeScript sources at:
+ https://github.com/gpuweb/cts
+
+ If you are debugging WebGPU conformance tests, it's highly recommended that
+ you use the standalone interactive runner in that repository, which
+ provides tools for easier debugging and editing (source maps, debug
+ logging, warn/skip functionality, etc.)
+
+ NOTE:
+ The WPT version of this file is generated with *one variant per test spec
+ file*. If your harness needs more fine-grained suppressions, you'll need to
+ generate your own variants list from your suppression list.
+ See `tools/gen_wpt_cts_html` to do this.
+
+ When run under browser CI, the original cts.https.html should be skipped, and
+ this alternate version should be run instead, under a non-exported WPT test
+ directory (e.g. Chromium's wpt_internal).
+-->
+
+<!doctype html>
+<title>WebGPU CTS</title>
+<meta charset=utf-8>
+<link rel=help href='https://gpuweb.github.io/gpuweb/'>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+ const loadWebGPUExpectations = undefined;
+ const shouldWebGPUCTSFailOnWarnings = undefined;
+</script>
+<script type=module src=/webgpu/common/runtime/wpt.js></script>
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/.eslintrc.json b/dom/webgpu/tests/cts/checkout/src/common/tools/.eslintrc.json
new file mode 100644
index 0000000000..aed978d459
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/.eslintrc.json
@@ -0,0 +1,9 @@
+{
+ "rules": {
+ "no-console": "off",
+ "no-process-exit": "off",
+ "node/no-unpublished-import": "off",
+ "node/no-unpublished-require": "off",
+ "@typescript-eslint/no-var-requires": "off"
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/checklist.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/checklist.ts
new file mode 100644
index 0000000000..393990e26f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/checklist.ts
@@ -0,0 +1,138 @@
+import * as fs from 'fs';
+import * as process from 'process';
+
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { Ordering, compareQueries } from '../internal/query/compare.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { TestQuery, TestQueryMultiFile } from '../internal/query/query.js';
+import { loadTreeForQuery, TestTree } from '../internal/tree.js';
+import { StacklessError } from '../internal/util.js';
+import { assert } from '../util/util.js';
+
+function usage(rc: number): void {
+ console.error('Usage:');
+ console.error(' tools/checklist FILE');
+ console.error(' tools/checklist my/list.txt');
+ process.exit(rc);
+}
+
+if (process.argv.length === 2) usage(0);
+if (process.argv.length !== 3) usage(1);
+
+type QueryInSuite = { readonly query: TestQuery; readonly done: boolean };
+type QueriesInSuite = QueryInSuite[];
+type QueriesBySuite = Map<string, QueriesInSuite>;
+async function loadQueryListFromTextFile(filename: string): Promise<QueriesBySuite> {
+ const lines = (await fs.promises.readFile(filename, 'utf8')).split(/\r?\n/);
+ const allQueries = lines
+ .filter(l => l)
+ .map(l => {
+ const [doneStr, q] = l.split(/\s+/);
+ assert(doneStr === 'DONE' || doneStr === 'TODO', 'first column must be DONE or TODO');
+ return { query: parseQuery(q), done: doneStr === 'DONE' } as const;
+ });
+
+ const queriesBySuite: QueriesBySuite = new Map();
+ for (const q of allQueries) {
+ let suiteQueries = queriesBySuite.get(q.query.suite);
+ if (suiteQueries === undefined) {
+ suiteQueries = [];
+ queriesBySuite.set(q.query.suite, suiteQueries);
+ }
+
+ suiteQueries.push(q);
+ }
+
+ return queriesBySuite;
+}
+
+function checkForOverlappingQueries(queries: QueriesInSuite): void {
+ for (let i1 = 0; i1 < queries.length; ++i1) {
+ for (let i2 = i1 + 1; i2 < queries.length; ++i2) {
+ const q1 = queries[i1].query;
+ const q2 = queries[i2].query;
+ if (compareQueries(q1, q2) !== Ordering.Unordered) {
+ console.log(` FYI, the following checklist items overlap:\n ${q1}\n ${q2}`);
+ }
+ }
+ }
+}
+
+function checkForUnmatchedSubtreesAndDoneness(
+ tree: TestTree,
+ matchQueries: QueriesInSuite
+): number {
+ let subtreeCount = 0;
+ const unmatchedSubtrees: TestQuery[] = [];
+ const overbroadMatches: [TestQuery, TestQuery][] = [];
+ const donenessMismatches: QueryInSuite[] = [];
+ const alwaysExpandThroughLevel = 1; // expand to, at minimum, every file.
+ for (const subtree of tree.iterateCollapsedNodes({
+ includeIntermediateNodes: true,
+ includeEmptySubtrees: true,
+ alwaysExpandThroughLevel,
+ })) {
+ subtreeCount++;
+ const subtreeDone = !subtree.subtreeCounts?.nodesWithTODO;
+
+ let subtreeMatched = false;
+ for (const q of matchQueries) {
+ const comparison = compareQueries(q.query, subtree.query);
+ if (comparison !== Ordering.Unordered) subtreeMatched = true;
+ if (comparison === Ordering.StrictSubset) continue;
+ if (comparison === Ordering.StrictSuperset) overbroadMatches.push([q.query, subtree.query]);
+ if (comparison === Ordering.Equal && q.done !== subtreeDone) donenessMismatches.push(q);
+ }
+ if (!subtreeMatched) unmatchedSubtrees.push(subtree.query);
+ }
+
+ if (overbroadMatches.length) {
+ // (note, this doesn't show ALL multi-test queries - just ones that actually match any .spec.ts)
+ console.log(` FYI, the following checklist items were broader than one file:`);
+ for (const [q, collapsedSubtree] of overbroadMatches) {
+ console.log(` ${q} > ${collapsedSubtree}`);
+ }
+ }
+
+ if (unmatchedSubtrees.length) {
+ throw new StacklessError(`Found unmatched tests:\n ${unmatchedSubtrees.join('\n ')}`);
+ }
+
+ if (donenessMismatches.length) {
+ throw new StacklessError(
+ 'Found done/todo mismatches:\n ' +
+ donenessMismatches
+ .map(q => `marked ${q.done ? 'DONE, but is TODO' : 'TODO, but is DONE'}: ${q.query}`)
+ .join('\n ')
+ );
+ }
+
+ return subtreeCount;
+}
+
+(async () => {
+ console.log('Loading queries...');
+ const queriesBySuite = await loadQueryListFromTextFile(process.argv[2]);
+ console.log(' Found suites: ' + Array.from(queriesBySuite.keys()).join(' '));
+
+ const loader = new DefaultTestFileLoader();
+ for (const [suite, queriesInSuite] of queriesBySuite.entries()) {
+ console.log(`Suite "${suite}":`);
+ console.log(` Checking overlaps between ${queriesInSuite.length} checklist items...`);
+ checkForOverlappingQueries(queriesInSuite);
+ const suiteQuery = new TestQueryMultiFile(suite, []);
+ console.log(` Loading tree ${suiteQuery}...`);
+ const tree = await loadTreeForQuery(
+ loader,
+ suiteQuery,
+ queriesInSuite.map(q => q.query)
+ );
+ console.log(' Found no invalid queries in the checklist. Checking for unmatched tests...');
+ const subtreeCount = checkForUnmatchedSubtreesAndDoneness(tree, queriesInSuite);
+ console.log(` No unmatched tests or done/todo mismatches among ${subtreeCount} subtrees!`);
+ }
+ console.log(`Checklist looks good!`);
+})().catch(ex => {
+ console.log(ex.stack ?? ex.toString());
+ process.exit(1);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts
new file mode 100644
index 0000000000..ae5cf41c2c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts
@@ -0,0 +1,102 @@
+// Node can look at the filesystem, but JS in the browser can't.
+// This crawls the file tree under src/suites/${suite} to generate a (non-hierarchical) static
+// listing file that can then be used in the browser to load the modules containing the tests.
+
+import * as fs from 'fs';
+import * as path from 'path';
+
+import { SpecFile } from '../internal/file_loader.js';
+import { validQueryPart } from '../internal/query/validQueryPart.js';
+import { TestSuiteListingEntry, TestSuiteListing } from '../internal/test_suite_listing.js';
+import { assert, unreachable } from '../util/util.js';
+
+const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js';
+
+async function crawlFilesRecursively(dir: string): Promise<string[]> {
+ const subpathInfo = await Promise.all(
+ (await fs.promises.readdir(dir)).map(async d => {
+ const p = path.join(dir, d);
+ const stats = await fs.promises.stat(p);
+ return {
+ path: p,
+ isDirectory: stats.isDirectory(),
+ isFile: stats.isFile(),
+ };
+ })
+ );
+
+ const files = subpathInfo
+ .filter(
+ i =>
+ i.isFile &&
+ (i.path.endsWith(specFileSuffix) ||
+ i.path.endsWith(`${path.sep}README.txt`) ||
+ i.path === 'README.txt')
+ )
+ .map(i => i.path);
+
+ return files.concat(
+ await subpathInfo
+ .filter(i => i.isDirectory)
+ .map(i => crawlFilesRecursively(i.path))
+ .reduce(async (a, b) => (await a).concat(await b), Promise.resolve([]))
+ );
+}
+
+export async function crawl(
+ suiteDir: string,
+ validate: boolean = true
+): Promise<TestSuiteListingEntry[]> {
+ if (!fs.existsSync(suiteDir)) {
+ console.error(`Could not find ${suiteDir}`);
+ process.exit(1);
+ }
+
+ // Crawl files and convert paths to be POSIX-style, relative to suiteDir.
+ const filesToEnumerate = (await crawlFilesRecursively(suiteDir))
+ .map(f => path.relative(suiteDir, f).replace(/\\/g, '/'))
+ .sort();
+
+ const entries: TestSuiteListingEntry[] = [];
+ for (const file of filesToEnumerate) {
+ // |file| is the suite-relative file path.
+ if (file.endsWith(specFileSuffix)) {
+ const filepathWithoutExtension = file.substring(0, file.length - specFileSuffix.length);
+
+ const suite = path.basename(suiteDir);
+
+ if (validate) {
+ const filename = `../../${suite}/${filepathWithoutExtension}.spec.js`;
+
+ assert(!process.env.STANDALONE_DEV_SERVER);
+ const mod = (await import(filename)) as SpecFile;
+ assert(mod.description !== undefined, 'Test spec file missing description: ' + filename);
+ assert(mod.g !== undefined, 'Test spec file missing TestGroup definition: ' + filename);
+
+ mod.g.validate();
+ }
+
+ const pathSegments = filepathWithoutExtension.split('/');
+ for (const p of pathSegments) {
+ assert(validQueryPart.test(p), `Invalid directory name ${p}; must match ${validQueryPart}`);
+ }
+ entries.push({ file: pathSegments });
+ } else if (path.basename(file) === 'README.txt') {
+ const dirname = path.dirname(file);
+ const readme = fs.readFileSync(path.join(suiteDir, file), 'utf8').trim();
+
+ const pathSegments = dirname !== '.' ? dirname.split('/') : [];
+ entries.push({ file: pathSegments, readme });
+ } else {
+ unreachable(`Matched an unrecognized filename ${file}`);
+ }
+ }
+
+ return entries;
+}
+
+export function makeListing(filename: string): Promise<TestSuiteListing> {
+ // Don't validate. This path is only used for the dev server and running tests with Node.
+ // Validation is done for listing generation and presubmit.
+ return crawl(path.dirname(filename), false);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts
new file mode 100644
index 0000000000..2e0aca21dd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts
@@ -0,0 +1,189 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+import * as babel from '@babel/core';
+import * as chokidar from 'chokidar';
+import * as express from 'express';
+import * as morgan from 'morgan';
+import * as portfinder from 'portfinder';
+import * as serveIndex from 'serve-index';
+
+import { makeListing } from './crawl.js';
+
+// Make sure that makeListing doesn't cache imported spec files. See crawl().
+process.env.STANDALONE_DEV_SERVER = '1';
+
+const srcDir = path.resolve(__dirname, '../../');
+
+// Import the project's babel.config.js. We'll use the same config for the runtime compiler.
+const babelConfig = {
+ ...require(path.resolve(srcDir, '../babel.config.js'))({
+ cache: () => {
+ /* not used */
+ },
+ }),
+ sourceMaps: 'inline',
+};
+
+// Caches for the generated listing file and compiled TS sources to speed up reloads.
+// Keyed by suite name
+const listingCache = new Map<string, string>();
+// Keyed by the path to the .ts file, without src/
+const compileCache = new Map<string, string>();
+
+console.log('Watching changes in', srcDir);
+const watcher = chokidar.watch(srcDir, {
+ persistent: true,
+});
+
+/**
+ * Handler to dirty the compile cache for changed .ts files.
+ */
+function dirtyCompileCache(absPath: string, stats?: fs.Stats) {
+ const relPath = path.relative(srcDir, absPath);
+ if ((stats === undefined || stats.isFile()) && relPath.endsWith('.ts')) {
+ const tsUrl = relPath;
+ if (compileCache.has(tsUrl)) {
+ console.debug('Dirtying compile cache', tsUrl);
+ }
+ compileCache.delete(tsUrl);
+ }
+}
+
+/**
+ * Handler to dirty the listing cache for:
+ * - Directory changes
+ * - .spec.ts changes
+ * - README.txt changes
+ * Also dirties the compile cache for changed files.
+ */
+function dirtyListingAndCompileCache(absPath: string, stats?: fs.Stats) {
+ const relPath = path.relative(srcDir, absPath);
+
+ const segments = relPath.split(path.sep);
+ // The listing changes if the directories change, or if a .spec.ts file is added/removed.
+ const listingChange =
+ // A directory or a file with no extension that we can't stat.
+ // (stat doesn't work for deletions)
+ ((path.extname(relPath) === '' && (stats === undefined || !stats.isFile())) ||
+ // A spec file
+ relPath.endsWith('.spec.ts') ||
+ // A README.txt
+ path.basename(relPath, 'txt') === 'README') &&
+ segments.length > 0;
+ if (listingChange) {
+ const suite = segments[0];
+ if (listingCache.has(suite)) {
+ console.debug('Dirtying listing cache', suite);
+ }
+ listingCache.delete(suite);
+ }
+
+ dirtyCompileCache(absPath, stats);
+}
+
+watcher.on('add', dirtyListingAndCompileCache);
+watcher.on('unlink', dirtyListingAndCompileCache);
+watcher.on('addDir', dirtyListingAndCompileCache);
+watcher.on('unlinkDir', dirtyListingAndCompileCache);
+watcher.on('change', dirtyCompileCache);
+
+const app = express();
+
+// Send Chrome Origin Trial tokens
+app.use((req, res, next) => {
+ res.header('Origin-Trial', [
+ // Token for http://localhost:8080
+ 'AvyDIV+RJoYs8fn3W6kIrBhWw0te0klraoz04mw/nPb8VTus3w5HCdy+vXqsSzomIH745CT6B5j1naHgWqt/tw8AAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjYzNzE4Mzk5fQ==',
+ ]);
+ next();
+});
+
+// Set up logging
+app.use(morgan('dev'));
+
+// Serve the standalone runner directory
+app.use('/standalone', express.static(path.resolve(srcDir, '../standalone')));
+// Add out-wpt/ build dir for convenience
+app.use('/out-wpt', express.static(path.resolve(srcDir, '../out-wpt')));
+app.use('/docs/tsdoc', express.static(path.resolve(srcDir, '../docs/tsdoc')));
+
+// Serve a suite's listing.js file by crawling the filesystem for all tests.
+app.get('/out/:suite/listing.js', async (req, res, next) => {
+ const suite = req.params['suite'];
+
+ if (listingCache.has(suite)) {
+ res.setHeader('Content-Type', 'application/javascript');
+ res.send(listingCache.get(suite));
+ return;
+ }
+
+ try {
+ const listing = await makeListing(path.resolve(srcDir, suite, 'listing.ts'));
+ const result = `export const listing = ${JSON.stringify(listing, undefined, 2)}`;
+
+ listingCache.set(suite, result);
+ res.setHeader('Content-Type', 'application/javascript');
+ res.send(result);
+ } catch (err) {
+ next(err);
+ }
+});
+
+// Serve all other .js files by fetching the source .ts file and compiling it.
+app.get('/out/**/*.js', async (req, res, next) => {
+ const jsUrl = path.relative('/out', req.url);
+ const tsUrl = jsUrl.replace(/\.js$/, '.ts');
+ if (compileCache.has(tsUrl)) {
+ res.setHeader('Content-Type', 'application/javascript');
+ res.send(compileCache.get(tsUrl));
+ return;
+ }
+
+ let absPath = path.join(srcDir, tsUrl);
+ if (!fs.existsSync(absPath)) {
+ // The .ts file doesn't exist. Try .js file in case this is a .js/.d.ts pair.
+ absPath = path.join(srcDir, jsUrl);
+ }
+
+ try {
+ const result = await babel.transformFileAsync(absPath, babelConfig);
+ if (result && result.code) {
+ compileCache.set(tsUrl, result.code);
+
+ res.setHeader('Content-Type', 'application/javascript');
+ res.send(result.code);
+ } else {
+ throw new Error(`Failed compile ${tsUrl}.`);
+ }
+ } catch (err) {
+ next(err);
+ }
+});
+
+const host = '0.0.0.0';
+const port = 8080;
+// Find an available port, starting at 8080.
+portfinder.getPort({ host, port }, (err, port) => {
+ if (err) {
+ throw err;
+ }
+ watcher.on('ready', () => {
+ // Listen on the available port.
+ app.listen(port, host, () => {
+ console.log('Standalone test runner running at:');
+ for (const iface of Object.values(os.networkInterfaces())) {
+ for (const details of iface || []) {
+ if (details.family === 'IPv4') {
+ console.log(` http://${details.address}:${port}/standalone/`);
+ }
+ }
+ }
+ });
+ });
+});
+
+// Serve everything else (not .js) as static, and directories as directory listings.
+app.use('/out', serveIndex(path.resolve(srcDir, '../src')));
+app.use('/out', express.static(path.resolve(srcDir, '../src')));
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts
new file mode 100644
index 0000000000..e7e6d8514f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts
@@ -0,0 +1,144 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as process from 'process';
+
+import { Cacheable, dataCache, setIsBuildingDataCache } from '../framework/data_cache.js';
+
+function usage(rc: number): void {
+ console.error(`Usage: tools/gen_cache [options] [OUT_DIR] [SUITE_DIRS...]
+
+For each suite in SUITE_DIRS, pre-compute data that is expensive to generate
+at runtime and store it under OUT_DIR. If the data file is found then the
+DataCache will load this instead of building the expensive data at CTS runtime.
+
+Options:
+ --help Print this message and exit.
+ --list Print the list of output files without writing them.
+`);
+ process.exit(rc);
+}
+
+let mode: 'emit' | 'list' = 'emit';
+
+const nonFlagsArgs: string[] = [];
+for (const a of process.argv) {
+ if (a.startsWith('-')) {
+ if (a === '--list') {
+ mode = 'list';
+ } else if (a === '--help') {
+ usage(0);
+ } else {
+ console.log('unrecognized flag: ', a);
+ usage(1);
+ }
+ } else {
+ nonFlagsArgs.push(a);
+ }
+}
+
+if (nonFlagsArgs.length < 4) {
+ usage(0);
+}
+
+const outRootDir = nonFlagsArgs[2];
+
+dataCache.setStore({
+ load: (path: string) => {
+ return new Promise<string>((resolve, reject) => {
+ fs.readFile(`data/${path}`, 'utf8', (err, data) => {
+ if (err !== null) {
+ reject(err.message);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ },
+});
+setIsBuildingDataCache();
+
+void (async () => {
+ for (const suiteDir of nonFlagsArgs.slice(3)) {
+ await build(suiteDir);
+ }
+})();
+
+const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js';
+
+async function crawlFilesRecursively(dir: string): Promise<string[]> {
+ const subpathInfo = await Promise.all(
+ (await fs.promises.readdir(dir)).map(async d => {
+ const p = path.join(dir, d);
+ const stats = await fs.promises.stat(p);
+ return {
+ path: p,
+ isDirectory: stats.isDirectory(),
+ isFile: stats.isFile(),
+ };
+ })
+ );
+
+ const files = subpathInfo
+ .filter(i => i.isFile && i.path.endsWith(specFileSuffix))
+ .map(i => i.path);
+
+ return files.concat(
+ await subpathInfo
+ .filter(i => i.isDirectory)
+ .map(i => crawlFilesRecursively(i.path))
+ .reduce(async (a, b) => (await a).concat(await b), Promise.resolve([]))
+ );
+}
+
+async function build(suiteDir: string) {
+ if (!fs.existsSync(suiteDir)) {
+ console.error(`Could not find ${suiteDir}`);
+ process.exit(1);
+ }
+
+ // Crawl files and convert paths to be POSIX-style, relative to suiteDir.
+ const filesToEnumerate = (await crawlFilesRecursively(suiteDir)).sort();
+
+ const cacheablePathToTS = new Map<string, string>();
+
+ for (const file of filesToEnumerate) {
+ if (file.endsWith(specFileSuffix)) {
+ const pathWithoutExtension = file.substring(0, file.length - specFileSuffix.length);
+ const mod = await import(`../../../${pathWithoutExtension}.spec.js`);
+ if (mod.d?.serialize !== undefined) {
+ const cacheable = mod.d as Cacheable<unknown>;
+
+ {
+ // Check for collisions
+ const existing = cacheablePathToTS.get(cacheable.path);
+ if (existing !== undefined) {
+ console.error(
+ `error: Cacheable '${cacheable.path}' is emitted by both:
+ '${existing}'
+and
+ '${file}'`
+ );
+ process.exit(1);
+ }
+ cacheablePathToTS.set(cacheable.path, file);
+ }
+
+ const outPath = `${outRootDir}/data/${cacheable.path}`;
+
+ switch (mode) {
+ case 'emit': {
+ const data = await cacheable.build();
+ const serialized = cacheable.serialize(data);
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
+ fs.writeFileSync(outPath, serialized);
+ break;
+ }
+ case 'list': {
+ console.log(outPath);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts
new file mode 100644
index 0000000000..7b7809c920
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts
@@ -0,0 +1,64 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as process from 'process';
+
+import { crawl } from './crawl.js';
+
+function usage(rc: number): void {
+ console.error(`Usage: tools/gen_listings [options] [OUT_DIR] [SUITE_DIRS...]
+
+For each suite in SUITE_DIRS, generate listings and write each listing.js
+into OUT_DIR/{suite}/listing.js. Example:
+ tools/gen_listings out/ src/unittests/ src/webgpu/
+
+Options:
+ --help Print this message and exit.
+ --no-validate Whether to validate test modules while crawling.
+`);
+ process.exit(rc);
+}
+
+const argv = process.argv;
+if (argv.indexOf('--help') !== -1) {
+ usage(0);
+}
+
+let validate = true;
+{
+ const i = argv.indexOf('--no-validate');
+ if (i !== -1) {
+ validate = false;
+ argv.splice(i, 1);
+ }
+}
+
+if (argv.length < 4) {
+ usage(0);
+}
+
+const myself = 'src/common/tools/gen_listings.ts';
+
+const outDir = argv[2];
+
+void (async () => {
+ for (const suiteDir of argv.slice(3)) {
+ const listing = await crawl(suiteDir, validate);
+
+ const suite = path.basename(suiteDir);
+ const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`));
+ fs.mkdirSync(path.join(outDir, suite), { recursive: true });
+ fs.writeFileSync(
+ outFile,
+ `\
+// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
+
+export const listing = ${JSON.stringify(listing, undefined, 2)};
+`
+ );
+ try {
+ fs.unlinkSync(outFile + '.map');
+ } catch (ex) {
+ // ignore if file didn't exist
+ }
+ }
+})();
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts
new file mode 100644
index 0000000000..28e8fb4437
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts
@@ -0,0 +1,122 @@
+import { promises as fs } from 'fs';
+
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { TestQueryMultiFile } from '../internal/query/query.js';
+import { assert } from '../util/util.js';
+
+function printUsageAndExit(rc: number): void {
+ console.error(`\
+Usage:
+ tools/gen_wpt_cts_html OUTPUT_FILE TEMPLATE_FILE [ARGUMENTS_PREFIXES_FILE EXPECTATIONS_FILE EXPECTATIONS_PREFIX [SUITE]]
+ tools/gen_wpt_cts_html out-wpt/cts.https.html templates/cts.https.html
+ tools/gen_wpt_cts_html my/path/to/cts.https.html templates/cts.https.html arguments.txt myexpectations.txt 'path/to/cts.https.html' cts
+
+where arguments.txt is a file containing a list of arguments prefixes to both generate and expect
+in the expectations. The entire variant list generation runs *once per prefix*, so this
+multiplies the size of the variant list.
+
+ ?worker=0&q=
+ ?worker=1&q=
+
+and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g.:
+
+ path/to/cts.https.html?worker=0&q=webgpu:a/foo:bar={"x":1}
+ path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":1}
+
+ path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":3}
+`);
+ process.exit(rc);
+}
+
+if (process.argv.length !== 4 && process.argv.length !== 7 && process.argv.length !== 8) {
+ printUsageAndExit(0);
+}
+
+const [
+ ,
+ ,
+ outFile,
+ templateFile,
+ argsPrefixesFile,
+ expectationsFile,
+ expectationsPrefix,
+ suite = 'webgpu',
+] = process.argv;
+
+(async () => {
+ let argsPrefixes = [''];
+ let expectationLines = new Set<string>();
+
+ if (process.argv.length >= 7) {
+ // Prefixes sorted from longest to shortest
+ const argsPrefixesFromFile = (await fs.readFile(argsPrefixesFile, 'utf8'))
+ .split(/\r?\n/)
+ .filter(a => a.length)
+ .sort((a, b) => b.length - a.length);
+ if (argsPrefixesFromFile.length) argsPrefixes = argsPrefixesFromFile;
+ expectationLines = new Set(
+ (await fs.readFile(expectationsFile, 'utf8')).split(/\r?\n/).filter(l => l.length)
+ );
+ }
+
+ const expectations: Map<string, string[]> = new Map();
+ for (const prefix of argsPrefixes) {
+ expectations.set(prefix, []);
+ }
+
+ expLoop: for (const exp of expectationLines) {
+ // Take each expectation for the longest prefix it matches.
+ for (const argsPrefix of argsPrefixes) {
+ const prefix = expectationsPrefix + argsPrefix;
+ if (exp.startsWith(prefix)) {
+ expectations.get(argsPrefix)!.push(exp.substring(prefix.length));
+ continue expLoop;
+ }
+ }
+ console.log('note: ignored expectation: ' + exp);
+ }
+
+ const loader = new DefaultTestFileLoader();
+ const lines: Array<string | undefined> = [];
+ for (const prefix of argsPrefixes) {
+ const rootQuery = new TestQueryMultiFile(suite, []);
+ const tree = await loader.loadTree(rootQuery, expectations.get(prefix));
+
+ lines.push(undefined); // output blank line between prefixes
+ const alwaysExpandThroughLevel = 2; // expand to, at minimum, every test.
+ for (const { query } of tree.iterateCollapsedNodes({ alwaysExpandThroughLevel })) {
+ const urlQueryString = prefix + query.toString(); // "?worker=0&q=..."
+ // Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole
+ // path must be <= 259. Leave room for e.g.:
+ // 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt'
+ assert(
+ urlQueryString.length < 185,
+ 'Generated test variant would produce too-long -actual.txt filename. \
+Try broadening suppressions to avoid long test variant names. ' +
+ urlQueryString
+ );
+ lines.push(urlQueryString);
+ }
+ }
+ await generateFile(lines);
+})().catch(ex => {
+ console.log(ex.stack ?? ex.toString());
+ process.exit(1);
+});
+
+async function generateFile(lines: Array<string | undefined>): Promise<void> {
+ let result = '';
+ result += '<!-- AUTO-GENERATED - DO NOT EDIT. See WebGPU CTS: tools/gen_wpt_cts_html. -->\n';
+
+ result += await fs.readFile(templateFile, 'utf8');
+
+ for (const line of lines) {
+ if (line === undefined) {
+ result += '\n';
+ } else {
+ result += `<meta name=variant content='${line}'>\n`;
+ }
+ }
+
+ await fs.writeFile(outFile, result);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/image_utils.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/image_utils.ts
new file mode 100644
index 0000000000..3c51cfdce3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/image_utils.ts
@@ -0,0 +1,58 @@
+import * as fs from 'fs';
+
+import { Page } from 'playwright-core';
+import { PNG } from 'pngjs';
+import { screenshot, WindowInfo } from 'screenshot-ftw';
+
+// eslint-disable-next-line ban/ban
+const waitMS = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+export function readPng(filename: string) {
+ const data = fs.readFileSync(filename);
+ return PNG.sync.read(data);
+}
+
+export function writePng(filename: string, width: number, height: number, data: Buffer) {
+ const png = new PNG({ colorType: 6, width, height });
+ for (let i = 0; i < data.byteLength; ++i) {
+ png.data[i] = data[i];
+ }
+ const buffer = PNG.sync.write(png);
+ fs.writeFileSync(filename, buffer);
+}
+
+export class ScreenshotManager {
+ window?: WindowInfo;
+
+ async init(page: Page) {
+ // set the title to some random number so we can find the window by title
+ const title: string = await page.evaluate(() => {
+ const title = `t-${Math.random()}`;
+ document.title = title;
+ return title;
+ });
+
+ // wait for the window to show up
+ let window;
+ for (let i = 0; !window && i < 100; ++i) {
+ await waitMS(50);
+ const windows = await screenshot.getWindows();
+ window = windows.find(window => window.title.includes(title));
+ }
+ if (!window) {
+ throw Error(`could not find window: ${title}`);
+ }
+ this.window = window;
+ }
+
+ async takeScreenshot(page: Page, screenshotName: string) {
+ // await page.screenshot({ path: screenshotName });
+
+ // we need to set the url and title since the screenshot will include the chrome
+ await page.evaluate(async () => {
+ document.title = 'screenshot';
+ window.history.replaceState({}, '', '/screenshot');
+ });
+ await screenshot.captureWindowById(screenshotName, this.window!.id);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/presubmit.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/presubmit.ts
new file mode 100644
index 0000000000..27505e759e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/presubmit.ts
@@ -0,0 +1,19 @@
+import { DefaultTestFileLoader } from '../internal/file_loader.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { assert } from '../util/util.js';
+
+void (async () => {
+ for (const suite of ['unittests', 'webgpu']) {
+ const loader = new DefaultTestFileLoader();
+ const filterQuery = parseQuery(`${suite}:*`);
+ const testcases = await loader.loadCases(filterQuery);
+ for (const testcase of testcases) {
+ const name = testcase.query.toString();
+ const maxLength = 375;
+ assert(
+ name.length <= maxLength,
+ `Testcase ${name} is too long. Max length is ${maxLength} characters. Please shorten names or reduce parameters.`
+ );
+ }
+ }
+})();
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/run_wpt_ref_tests.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/run_wpt_ref_tests.ts
new file mode 100644
index 0000000000..42ff60001c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/run_wpt_ref_tests.ts
@@ -0,0 +1,446 @@
+import * as fs from 'fs';
+import * as path from 'path';
+
+import { chromium, firefox, webkit, Page, Browser } from 'playwright-core';
+
+import { ScreenshotManager, readPng, writePng } from './image_utils.js';
+
+declare function wptRefTestPageReady(): boolean;
+declare function wptRefTestGetTimeout(): boolean;
+
+const verbose = !!process.env.VERBOSE;
+const kRefTestsBaseURL = 'http://localhost:8080/out/webgpu/web_platform/reftests';
+const kRefTestsPath = 'src/webgpu/web_platform/reftests';
+const kScreenshotPath = 'out-wpt-reftest-screenshots';
+
+// note: technically we should use an HTML parser to find this to deal with whitespace
+// attribute order, quotes, entities, etc but since we control the test source we can just
+// make sure they match
+const kRefLinkRE = /<link\s+rel="match"\s+href="(.*?)"/;
+const kRefWaitClassRE = /class="reftest-wait"/;
+const kFuzzy = /<meta\s+name="?fuzzy"?\s+content="(.*?)">/;
+
+function printUsage() {
+ console.log(`
+run_wpt_ref_tests path-to-browser-executable [ref-test-name]
+
+where ref-test-name is just a simple check for the test including the given string.
+If not passed all ref tests are run
+
+MacOS Chrome Example:
+ node tools/run_wpt_ref_tests /Applications/Google\\ Chrome\\ Canary.app/Contents/MacOS/Google\\ Chrome\\ Canary
+
+`);
+}
+
+// Get all of filenames that end with '.html'
+function getRefTestNames(refTestPath: string) {
+ return fs.readdirSync(refTestPath).filter(name => name.endsWith('.html'));
+}
+
+// Given a regex with one capture, return it or the empty string if no match.
+function getRegexMatchCapture(re: RegExp, content: string) {
+ const m = re.exec(content);
+ return m ? m[1] : '';
+}
+
+type FileInfo = {
+ content: string;
+ refLink: string;
+ refWait: boolean;
+ fuzzy: string;
+};
+
+function readHTMLFile(filename: string): FileInfo {
+ const content = fs.readFileSync(filename, { encoding: 'utf8' });
+ return {
+ content,
+ refLink: getRegexMatchCapture(kRefLinkRE, content),
+ refWait: kRefWaitClassRE.test(content),
+ fuzzy: getRegexMatchCapture(kFuzzy, content),
+ };
+}
+
+/**
+ * This is workaround for a bug in Chrome. The bug is when in emulation mode
+ * Chrome lets you set a devicePixelRatio but Chrome still renders in the
+ * actual devicePixelRatio, at least on MacOS.
+ * So, we compute the ratio and then use that.
+ */
+async function getComputedDevicePixelRatio(browser: Browser): Promise<number> {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ await page.goto('data:text/html,<html></html>');
+ await page.waitForLoadState('networkidle');
+ const devicePixelRatio = await page.evaluate(() => {
+ let resolve: (v: number) => void;
+ const promise = new Promise(_resolve => (resolve = _resolve));
+ const observer = new ResizeObserver(entries => {
+ const devicePixelWidth = entries[0].devicePixelContentBoxSize[0].inlineSize;
+ const clientWidth = entries[0].target.clientWidth;
+ const devicePixelRatio = devicePixelWidth / clientWidth;
+ resolve(devicePixelRatio);
+ });
+ observer.observe(document.documentElement);
+ return promise;
+ });
+ await page.close();
+ await context.close();
+ return devicePixelRatio as number;
+}
+
+// Note: If possible, rather then start adding command line options to this tool,
+// see if you can just make it work based off the path.
+async function getBrowserInterface(executablePath: string) {
+ const lc = executablePath.toLowerCase();
+ if (lc.includes('chrom')) {
+ const browser = await chromium.launch({
+ executablePath,
+ headless: false,
+ args: ['--enable-unsafe-webgpu'],
+ });
+ const devicePixelRatio = await getComputedDevicePixelRatio(browser);
+ const context = await browser.newContext({
+ deviceScaleFactor: devicePixelRatio,
+ });
+ return { browser, context };
+ } else if (lc.includes('firefox')) {
+ const browser = await firefox.launch({
+ executablePath,
+ headless: false,
+ });
+ const context = await browser.newContext();
+ return { browser, context };
+ } else if (lc.includes('safari') || lc.includes('webkit')) {
+ const browser = await webkit.launch({
+ executablePath,
+ headless: false,
+ });
+ const context = await browser.newContext();
+ return { browser, context };
+ } else {
+ throw new Error(`could not guess browser from executable path: ${executablePath}`);
+ }
+}
+
+// Parses a fuzzy spec as defined here
+// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
+// Note: This is not robust but the tests will eventually be run in the real wpt.
+function parseFuzzy(fuzzy: string) {
+ if (!fuzzy) {
+ return { maxDifference: [0, 0], totalPixels: [0, 0] };
+ } else {
+ const parts = fuzzy.split(';');
+ if (parts.length !== 2) {
+ throw Error(`unhandled fuzzy format: ${fuzzy}`);
+ }
+ const ranges = parts.map(part => {
+ const range = part
+ .replace(/[a-zA-Z=]/g, '')
+ .split('-')
+ .map(v => parseInt(v));
+ return range.length === 1 ? [0, range[0]] : range;
+ });
+ return {
+ maxDifference: ranges[0],
+ totalPixels: ranges[1],
+ };
+ }
+}
+
+// Compares two images using the algorithm described in the web platform tests
+// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
+// If they are different will write out a diff mask.
+async function compareImages(
+ filename1: string,
+ filename2: string,
+ fuzzy: string,
+ diffName: string,
+ startingRow: number = 0
+) {
+ const img1 = readPng(filename1);
+ const img2 = readPng(filename2);
+ const { width, height } = img1;
+ if (img2.width !== width || img2.height !== height) {
+ console.error('images are not the same size:', filename1, filename2);
+ return;
+ }
+
+ const { maxDifference, totalPixels } = parseFuzzy(fuzzy);
+
+ const diffData = Buffer.alloc(width * height * 4);
+ const diffPixels = new Uint32Array(diffData.buffer);
+ const kRed = 0xff0000ff;
+ const kWhite = 0xffffffff;
+ const kYellow = 0xff00ffff;
+
+ let numPixelsDifferent = 0;
+ let anyPixelsOutOfRange = false;
+ for (let y = startingRow; y < height; ++y) {
+ for (let x = 0; x < width; ++x) {
+ const offset = y * width + x;
+ let isDifferent = false;
+ let outOfRange = false;
+ for (let c = 0; c < 4 && !outOfRange; ++c) {
+ const off = offset * 4 + c;
+ const v0 = img1.data[off];
+ const v1 = img2.data[off];
+ const channelDiff = Math.abs(v0 - v1);
+ outOfRange ||= channelDiff < maxDifference[0] || channelDiff > maxDifference[1];
+ isDifferent ||= channelDiff > 0;
+ }
+ numPixelsDifferent += isDifferent ? 1 : 0;
+ anyPixelsOutOfRange ||= outOfRange;
+ diffPixels[offset] = outOfRange ? kRed : isDifferent ? kYellow : kWhite;
+ }
+ }
+
+ const pass =
+ !anyPixelsOutOfRange &&
+ numPixelsDifferent >= totalPixels[0] &&
+ numPixelsDifferent <= totalPixels[1];
+ if (!pass) {
+ writePng(diffName, width, height, diffData);
+ console.error(
+ `FAIL: too many differences in: ${filename1} vs ${filename2}
+ ${numPixelsDifferent} differences, expected: ${totalPixels[0]}-${totalPixels[1]} with range: ${maxDifference[0]}-${maxDifference[1]}
+ wrote difference to: ${diffName};
+ `
+ );
+ } else {
+ console.log(`PASS`);
+ }
+ return pass;
+}
+
+function exists(filename: string) {
+ try {
+ fs.accessSync(filename);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+async function waitForPageRender(page: Page) {
+ await page.evaluate(() => {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+ });
+}
+
+// returns true if the page timed out.
+async function runPage(page: Page, url: string, refWait: boolean) {
+ console.log(' loading:', url);
+ // we need to load about:blank to force the browser to re-render
+ // else the previous page may still be visible if the page we are loading fails
+ await page.goto('about:blank');
+ await page.waitForLoadState('domcontentloaded');
+ await waitForPageRender(page);
+
+ await page.goto(url);
+ await page.waitForLoadState('domcontentloaded');
+ await waitForPageRender(page);
+
+ if (refWait) {
+ await page.waitForFunction(() => wptRefTestPageReady());
+ const timeout = await page.evaluate(() => wptRefTestGetTimeout());
+ if (timeout) {
+ return true;
+ }
+ }
+ return false;
+}
+
+async function main() {
+ const args = process.argv.slice(2);
+ if (args.length < 1 || args.length > 2) {
+ printUsage();
+ return;
+ }
+
+ const [executablePath, refTestName] = args;
+
+ if (!exists(executablePath)) {
+ console.error(executablePath, 'does not exist');
+ return;
+ }
+
+ const testNames = getRefTestNames(kRefTestsPath).filter(name =>
+ refTestName ? name.includes(refTestName) : true
+ );
+
+ if (!exists(kScreenshotPath)) {
+ fs.mkdirSync(kScreenshotPath, { recursive: true });
+ }
+
+ if (testNames.length === 0) {
+ console.error(`no tests include "${refTestName}"`);
+ return;
+ }
+
+ const { browser, context } = await getBrowserInterface(executablePath);
+ const page = await context.newPage();
+
+ const screenshotManager = new ScreenshotManager();
+ await screenshotManager.init(page);
+
+ if (verbose) {
+ page.on('console', async msg => {
+ const { url, lineNumber, columnNumber } = msg.location();
+ const values = await Promise.all(msg.args().map(a => a.jsonValue()));
+ console.log(`${url}:${lineNumber}:${columnNumber}:`, ...values);
+ });
+ }
+
+ await page.addInitScript({
+ content: `
+ (() => {
+ let timeout = false;
+ setTimeout(() => timeout = true, 5000);
+
+ window.wptRefTestPageReady = function() {
+ return timeout || !document.documentElement.classList.contains('reftest-wait');
+ };
+
+ window.wptRefTestGetTimeout = function() {
+ return timeout;
+ };
+ })();
+ `,
+ });
+
+ type Result = {
+ status: string;
+ testName: string;
+ refName: string;
+ testScreenshotName: string;
+ refScreenshotName: string;
+ diffName: string;
+ };
+ const results: Result[] = [];
+ const addResult = (
+ status: string,
+ testName: string,
+ refName: string,
+ testScreenshotName: string = '',
+ refScreenshotName: string = '',
+ diffName: string = ''
+ ) => {
+ results.push({ status, testName, refName, testScreenshotName, refScreenshotName, diffName });
+ };
+
+ for (const testName of testNames) {
+ console.log('processing:', testName);
+ const { refLink, refWait, fuzzy } = readHTMLFile(path.join(kRefTestsPath, testName));
+ if (!refLink) {
+ throw new Error(`could not find ref link in: ${testName}`);
+ }
+ const testURL = `${kRefTestsBaseURL}/${testName}`;
+ const refURL = `${kRefTestsBaseURL}/${refLink}`;
+
+ // Technically this is not correct but it fits the existing tests.
+ // It assumes refLink is relative to the refTestsPath but it's actually
+ // supposed to be relative to the test. It might also be an absolute
+ // path. Neither of those cases exist at the time of writing this.
+ const refFileInfo = readHTMLFile(path.join(kRefTestsPath, refLink));
+ const testScreenshotName = path.join(kScreenshotPath, `${testName}-actual.png`);
+ const refScreenshotName = path.join(kScreenshotPath, `${testName}-expected.png`);
+ const diffName = path.join(kScreenshotPath, `${testName}-diff.png`);
+
+ const timeoutTest = await runPage(page, testURL, refWait);
+ if (timeoutTest) {
+ addResult('TIMEOUT', testName, refLink);
+ continue;
+ }
+ await screenshotManager.takeScreenshot(page, testScreenshotName);
+
+ const timeoutRef = await runPage(page, refURL, refFileInfo.refWait);
+ if (timeoutRef) {
+ addResult('TIMEOUT', testName, refLink);
+ continue;
+ }
+ await screenshotManager.takeScreenshot(page, refScreenshotName);
+
+ const pass = await compareImages(testScreenshotName, refScreenshotName, fuzzy, diffName);
+ addResult(
+ pass ? 'PASS' : 'FAILURE',
+ testName,
+ refLink,
+ testScreenshotName,
+ refScreenshotName,
+ diffName
+ );
+ }
+
+ console.log(
+ `----results----\n${results
+ .map(({ status, testName }) => `[ ${status.padEnd(7)} ] ${testName}`)
+ .join('\n')}`
+ );
+
+ const imgLink = (filename: string, title: string) => {
+ const name = path.basename(filename);
+ return `
+ <div class="screenshot">
+ ${title}
+ <a href="${name}" title="${name}">
+ <img src="${name}" width="256"/>
+ </a>
+ </div>`;
+ };
+
+ const indexName = path.join(kScreenshotPath, 'index.html');
+ fs.writeFileSync(
+ indexName,
+ `<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ .screenshot {
+ display: inline-block;
+ background: #CCC;
+ margin-right: 5px;
+ padding: 5px;
+ }
+ .screenshot a {
+ display: block;
+ }
+ .screenshot
+ </style>
+ </head>
+ <body>
+ ${results
+ .map(({ status, testName, refName, testScreenshotName, refScreenshotName, diffName }) => {
+ return `
+ <div>
+ <div>[ ${status} ]: ${testName} ref: ${refName}</div>
+ ${
+ status === 'FAILURE'
+ ? `${imgLink(testScreenshotName, 'actual')}
+ ${imgLink(refScreenshotName, 'ref')}
+ ${imgLink(diffName, 'diff')}`
+ : ``
+ }
+ </div>
+ <hr>
+ `;
+ })
+ .join('\n')}
+ </body>
+</html>
+ `
+ );
+
+ // the file:// with an absolute path makes it clickable in some terminals
+ console.log(`\nsee: file://${path.resolve(indexName)}\n`);
+
+ await page.close();
+ await context.close();
+ // I have no idea why it's taking ~30 seconds for playwright to close.
+ console.log('-- [ done: waiting for browser to close ] --');
+ await browser.close();
+}
+
+main().catch(e => {
+ throw e;
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/setup-ts-in-node.js b/dom/webgpu/tests/cts/checkout/src/common/tools/setup-ts-in-node.js
new file mode 100644
index 0000000000..89e91e8c9d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/setup-ts-in-node.js
@@ -0,0 +1,51 @@
+const path = require('path');
+
+// Automatically transpile .ts imports
+require('ts-node').register({
+ // Specify the project file so ts-node doesn't try to find it itself based on the CWD.
+ project: path.resolve(__dirname, '../../../tsconfig.json'),
+ compilerOptions: {
+ module: 'commonjs',
+ },
+ transpileOnly: true,
+});
+const Module = require('module');
+
+// Redirect imports of .js files to .ts files
+const resolveFilename = Module._resolveFilename;
+Module._resolveFilename = (request, parentModule, isMain) => {
+ do {
+ if (request.startsWith('.') && parentModule.filename.endsWith('.ts')) {
+ // Required for browser (because it needs the actual correct file path and
+ // can't do any kind of file resolution).
+ if (request.endsWith('/index.js')) {
+ throw new Error(
+ "Avoid the name `index.js`; we don't have Node-style path resolution: " + request
+ );
+ }
+
+ // Import of Node addon modules are valid and should pass through.
+ if (request.endsWith('.node')) {
+ break;
+ }
+
+ if (!request.endsWith('.js')) {
+ throw new Error('All relative imports must end in .js: ' + request);
+ }
+
+ try {
+ const tsRequest = request.substring(0, request.length - '.js'.length) + '.ts';
+ return resolveFilename.call(this, tsRequest, parentModule, isMain);
+ } catch (ex) {
+ // If the .ts file doesn't exist, try .js instead.
+ break;
+ }
+ }
+ } while (0);
+
+ return resolveFilename.call(this, request, parentModule, isMain);
+};
+
+process.on('unhandledRejection', ex => {
+ throw ex;
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/version.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/version.ts
new file mode 100644
index 0000000000..2b51700b12
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/tools/version.ts
@@ -0,0 +1,4 @@
+export const version = require('child_process')
+ .execSync('git describe --always --abbrev=0 --dirty')
+ .toString()
+ .trim();
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts b/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts
new file mode 100644
index 0000000000..670028d41c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts
@@ -0,0 +1,58 @@
+import { resolveOnTimeout } from './util.js';
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+declare const Components: any;
+
+/**
+ * Attempts to trigger JavaScript garbage collection, either using explicit methods if exposed
+ * (may be available in testing environments with special browser runtime flags set), or using
+ * some weird tricks to incur GC pressure. Adopted from the WebGL CTS.
+ */
+export async function attemptGarbageCollection(): Promise<void> {
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ const w: any = globalThis;
+ if (w.GCController) {
+ w.GCController.collect();
+ return;
+ }
+
+ if (w.opera && w.opera.collect) {
+ w.opera.collect();
+ return;
+ }
+
+ try {
+ w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .garbageCollect();
+ return;
+ } catch (e) {
+ // ignore any failure
+ }
+
+ if (w.gc) {
+ w.gc();
+ return;
+ }
+
+ if (w.CollectGarbage) {
+ w.CollectGarbage();
+ return;
+ }
+
+ let i: number;
+ function gcRec(n: number): void {
+ if (n < 1) return;
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
+ let temp: object | string = { i: 'ab' + i + i / 100000 };
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
+ temp = temp + 'foo';
+ temp; // dummy use of unused variable
+ gcRec(n - 1);
+ }
+ for (i = 0; i < 1000; i++) {
+ gcRec(10);
+ }
+
+ return resolveOnTimeout(35); // Let the event loop run a few frames in case it helps.
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts b/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts
new file mode 100644
index 0000000000..709d159320
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts
@@ -0,0 +1,127 @@
+/**
+ * The interface used for formatting strings to contain color metadata.
+ *
+ * Use the interface properties to construct a style, then use the
+ * `(s: string): string` function to format the provided string with the given
+ * style.
+ */
+export interface Colors {
+ // Are colors enabled?
+ enabled: boolean;
+
+ // Returns the string formatted to contain the specified color or style.
+ (s: string): string;
+
+ // modifiers
+ reset: Colors;
+ bold: Colors;
+ dim: Colors;
+ italic: Colors;
+ underline: Colors;
+ inverse: Colors;
+ hidden: Colors;
+ strikethrough: Colors;
+
+ // colors
+ black: Colors;
+ red: Colors;
+ green: Colors;
+ yellow: Colors;
+ blue: Colors;
+ magenta: Colors;
+ cyan: Colors;
+ white: Colors;
+ gray: Colors;
+ grey: Colors;
+
+ // bright colors
+ blackBright: Colors;
+ redBright: Colors;
+ greenBright: Colors;
+ yellowBright: Colors;
+ blueBright: Colors;
+ magentaBright: Colors;
+ cyanBright: Colors;
+ whiteBright: Colors;
+
+ // background colors
+ bgBlack: Colors;
+ bgRed: Colors;
+ bgGreen: Colors;
+ bgYellow: Colors;
+ bgBlue: Colors;
+ bgMagenta: Colors;
+ bgCyan: Colors;
+ bgWhite: Colors;
+
+ // bright background colors
+ bgBlackBright: Colors;
+ bgRedBright: Colors;
+ bgGreenBright: Colors;
+ bgYellowBright: Colors;
+ bgBlueBright: Colors;
+ bgMagentaBright: Colors;
+ bgCyanBright: Colors;
+ bgWhiteBright: Colors;
+}
+
+/**
+ * The interface used for formatting strings with color metadata.
+ *
+ * Currently Colors will use the 'ansi-colors' module if it can be loaded.
+ * If it cannot be loaded, then the Colors implementation is a straight pass-through.
+ *
+ * Colors may also be a no-op if the current environment does not support colors.
+ */
+export let Colors: Colors;
+
+try {
+ /* eslint-disable-next-line node/no-unpublished-require */
+ Colors = require('ansi-colors') as Colors;
+} catch {
+ const passthrough = ((s: string) => s) as Colors;
+ passthrough.enabled = false;
+ passthrough.reset = passthrough;
+ passthrough.bold = passthrough;
+ passthrough.dim = passthrough;
+ passthrough.italic = passthrough;
+ passthrough.underline = passthrough;
+ passthrough.inverse = passthrough;
+ passthrough.hidden = passthrough;
+ passthrough.strikethrough = passthrough;
+ passthrough.black = passthrough;
+ passthrough.red = passthrough;
+ passthrough.green = passthrough;
+ passthrough.yellow = passthrough;
+ passthrough.blue = passthrough;
+ passthrough.magenta = passthrough;
+ passthrough.cyan = passthrough;
+ passthrough.white = passthrough;
+ passthrough.gray = passthrough;
+ passthrough.grey = passthrough;
+ passthrough.blackBright = passthrough;
+ passthrough.redBright = passthrough;
+ passthrough.greenBright = passthrough;
+ passthrough.yellowBright = passthrough;
+ passthrough.blueBright = passthrough;
+ passthrough.magentaBright = passthrough;
+ passthrough.cyanBright = passthrough;
+ passthrough.whiteBright = passthrough;
+ passthrough.bgBlack = passthrough;
+ passthrough.bgRed = passthrough;
+ passthrough.bgGreen = passthrough;
+ passthrough.bgYellow = passthrough;
+ passthrough.bgBlue = passthrough;
+ passthrough.bgMagenta = passthrough;
+ passthrough.bgCyan = passthrough;
+ passthrough.bgWhite = passthrough;
+ passthrough.bgBlackBright = passthrough;
+ passthrough.bgRedBright = passthrough;
+ passthrough.bgGreenBright = passthrough;
+ passthrough.bgYellowBright = passthrough;
+ passthrough.bgBlueBright = passthrough;
+ passthrough.bgMagentaBright = passthrough;
+ passthrough.bgCyanBright = passthrough;
+ passthrough.bgWhiteBright = passthrough;
+ Colors = passthrough;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts b/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts
new file mode 100644
index 0000000000..7f1be2f701
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts
@@ -0,0 +1,39 @@
+import { ResolveType, ZipKeysWithValues } from './types.js';
+
+export type valueof<K> = K[keyof K];
+
+export function keysOf<T extends string>(obj: { [k in T]: unknown }): readonly T[] {
+ return (Object.keys(obj) as unknown[]) as T[];
+}
+
+export function numericKeysOf<T>(obj: object): readonly T[] {
+ return (Object.keys(obj).map(n => Number(n)) as unknown[]) as T[];
+}
+
+/**
+ * Creates an info lookup object from a more nicely-formatted table. See below for examples.
+ *
+ * Note: Using `as const` on the arguments to this function is necessary to infer the correct type.
+ */
+export function makeTable<
+ Members extends readonly string[],
+ Defaults extends readonly unknown[],
+ Table extends { readonly [k: string]: readonly unknown[] }
+>(
+ members: Members,
+ defaults: Defaults,
+ table: Table
+): {
+ readonly [k in keyof Table]: ResolveType<ZipKeysWithValues<Members, Table[k], Defaults>>;
+} {
+ const result: { [k: string]: { [m: string]: unknown } } = {};
+ for (const [k, v] of Object.entries<readonly unknown[]>(table)) {
+ const item: { [m: string]: unknown } = {};
+ for (let i = 0; i < members.length; ++i) {
+ item[members[i]] = v[i] ?? defaults[i];
+ }
+ result[k] = item;
+ }
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ return result as any;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts b/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts
new file mode 100644
index 0000000000..47cb1a4701
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts
@@ -0,0 +1,74 @@
+/// <reference types="@webgpu/types" />
+
+import { assert } from './util.js';
+
+/**
+ * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
+ * Throws an exception if not found.
+ */
+function defaultGPUProvider(): GPU {
+ assert(
+ typeof navigator !== 'undefined' && navigator.gpu !== undefined,
+ 'No WebGPU implementation found'
+ );
+ return navigator.gpu;
+}
+
+/**
+ * GPUProvider is a function that creates and returns a new GPU instance.
+ * May throw an exception if a GPU cannot be created.
+ */
+export type GPUProvider = () => GPU;
+
+let gpuProvider: GPUProvider = defaultGPUProvider;
+
+/**
+ * Sets the function to create and return a new GPU instance.
+ */
+export function setGPUProvider(provider: GPUProvider) {
+ assert(impl === undefined, 'setGPUProvider() should not be after getGPU()');
+ gpuProvider = provider;
+}
+
+let impl: GPU | undefined = undefined;
+
+let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined;
+
+export function setDefaultRequestAdapterOptions(options: GPURequestAdapterOptions) {
+ if (impl) {
+ throw new Error('must call setDefaultRequestAdapterOptions before getGPU');
+ }
+ defaultRequestAdapterOptions = { ...options };
+}
+
+/**
+ * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
+ * Throws an exception if not found.
+ */
+export function getGPU(): GPU {
+ if (impl) {
+ return impl;
+ }
+
+ impl = gpuProvider();
+
+ if (defaultRequestAdapterOptions) {
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ const oldFn = impl.requestAdapter;
+ impl.requestAdapter = function (
+ options?: GPURequestAdapterOptions
+ ): Promise<GPUAdapter | null> {
+ const promise = oldFn.call(this, { ...defaultRequestAdapterOptions, ...(options || {}) });
+ void promise.then(async adapter => {
+ if (adapter) {
+ const info = await adapter.requestAdapterInfo();
+ // eslint-disable-next-line no-console
+ console.log(info);
+ }
+ });
+ return promise;
+ };
+ }
+
+ return impl;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts b/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts
new file mode 100644
index 0000000000..7dc2822498
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts
@@ -0,0 +1,149 @@
+import { assert } from './util.js';
+
+// The state of the preprocessor is a stack of States.
+type StateStack = { allowsFollowingElse: boolean; state: State }[];
+const enum State {
+ Seeking, // Still looking for a passing condition
+ Passing, // Currently inside a passing condition (the root is always in this state)
+ Skipping, // Have already seen a passing condition; now skipping the rest
+}
+
+// The transitions in the state space are the following preprocessor directives:
+// - Sibling elif
+// - Sibling else
+// - Sibling endif
+// - Child if
+abstract class Directive {
+ private readonly depth: number;
+
+ constructor(depth: number) {
+ this.depth = depth;
+ }
+
+ protected checkDepth(stack: StateStack): void {
+ assert(
+ stack.length === this.depth,
+ `Number of "$"s must match nesting depth, currently ${stack.length} (e.g. $if $$if $$endif $endif)`
+ );
+ }
+
+ abstract applyTo(stack: StateStack): void;
+}
+
+class If extends Directive {
+ private readonly predicate: boolean;
+
+ constructor(depth: number, predicate: boolean) {
+ super(depth);
+ this.predicate = predicate;
+ }
+
+ applyTo(stack: StateStack) {
+ this.checkDepth(stack);
+ const parentState = stack[stack.length - 1].state;
+ stack.push({
+ allowsFollowingElse: true,
+ state:
+ parentState !== State.Passing
+ ? State.Skipping
+ : this.predicate
+ ? State.Passing
+ : State.Seeking,
+ });
+ }
+}
+
+class ElseIf extends If {
+ applyTo(stack: StateStack) {
+ assert(stack.length >= 1);
+ const { allowsFollowingElse, state: siblingState } = stack.pop()!;
+ this.checkDepth(stack);
+ assert(allowsFollowingElse, 'pp.elif after pp.else');
+ if (siblingState !== State.Seeking) {
+ stack.push({ allowsFollowingElse: true, state: State.Skipping });
+ } else {
+ super.applyTo(stack);
+ }
+ }
+}
+
+class Else extends Directive {
+ applyTo(stack: StateStack) {
+ assert(stack.length >= 1);
+ const { allowsFollowingElse, state: siblingState } = stack.pop()!;
+ this.checkDepth(stack);
+ assert(allowsFollowingElse, 'pp.else after pp.else');
+ stack.push({
+ allowsFollowingElse: false,
+ state: siblingState === State.Seeking ? State.Passing : State.Skipping,
+ });
+ }
+}
+
+class EndIf extends Directive {
+ applyTo(stack: StateStack) {
+ stack.pop();
+ this.checkDepth(stack);
+ }
+}
+
+/**
+ * A simple template-based, non-line-based preprocessor implementing if/elif/else/endif.
+ *
+ * @example
+ * ```
+ * const shader = pp`
+ * ${pp._if(expr)}
+ * const x: ${type} = ${value};
+ * ${pp._elif(expr)}
+ * ${pp.__if(expr)}
+ * ...
+ * ${pp.__else}
+ * ...
+ * ${pp.__endif}
+ * ${pp._endif}`;
+ * ```
+ *
+ * @param strings - The array of constant string chunks of the template string.
+ * @param ...values - The array of interpolated `${}` values within the template string.
+ */
+export function pp(
+ strings: TemplateStringsArray,
+ ...values: ReadonlyArray<Directive | string | number>
+): string {
+ let result = '';
+ const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }];
+
+ for (let i = 0; i < values.length; ++i) {
+ const passing = stateStack[stateStack.length - 1].state === State.Passing;
+ if (passing) {
+ result += strings[i];
+ }
+
+ const value = values[i];
+ if (value instanceof Directive) {
+ value.applyTo(stateStack);
+ } else {
+ if (passing) {
+ result += value;
+ }
+ }
+ }
+ assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file');
+ result += strings[values.length];
+
+ return result;
+}
+pp._if = (predicate: boolean) => new If(1, predicate);
+pp._elif = (predicate: boolean) => new ElseIf(1, predicate);
+pp._else = new Else(1);
+pp._endif = new EndIf(1);
+pp.__if = (predicate: boolean) => new If(2, predicate);
+pp.__elif = (predicate: boolean) => new ElseIf(2, predicate);
+pp.__else = new Else(2);
+pp.__endif = new EndIf(2);
+pp.___if = (predicate: boolean) => new If(3, predicate);
+pp.___elif = (predicate: boolean) => new ElseIf(3, predicate);
+pp.___else = new Else(3);
+pp.___endif = new EndIf(3);
+// Add more if needed.
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts b/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts
new file mode 100644
index 0000000000..13c3b7fb90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts
@@ -0,0 +1,7 @@
+/** Defined by WPT. Like `setTimeout`, but applies a timeout multiplier for slow test systems. */
+declare const step_timeout: undefined | typeof setTimeout;
+
+/**
+ * Equivalent of `setTimeout`, but redirects to WPT's `step_timeout` when it is defined.
+ */
+export const timeout = typeof step_timeout !== 'undefined' ? step_timeout : setTimeout;
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/types.ts b/dom/webgpu/tests/cts/checkout/src/common/util/types.ts
new file mode 100644
index 0000000000..dfd5e4b5ea
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/types.ts
@@ -0,0 +1,59 @@
+/** Forces a type to resolve its type definitions, to make it readable/debuggable. */
+export type ResolveType<T> = T extends object
+ ? T extends infer O
+ ? { [K in keyof O]: ResolveType<O[K]> }
+ : never
+ : T;
+
+/** Returns the type `true` iff X and Y are exactly equal */
+export type TypeEqual<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
+ ? true
+ : false;
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+export function assertTypeTrue<T extends true>() {}
+
+/**
+ * Computes the intersection of a set of types, given the union of those types.
+ *
+ * From: https://stackoverflow.com/a/56375136
+ */
+export type UnionToIntersection<U> =
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
+
+/** "Type asserts" that `X` is a subtype of `Y`. */
+type EnsureSubtype<X, Y> = X extends Y ? X : never;
+
+type TupleHeadOr<T, Default> = T extends readonly [infer H, ...(readonly unknown[])] ? H : Default;
+type TupleTailOr<T, Default> = T extends readonly [unknown, ...infer Tail] ? Tail : Default;
+type TypeOr<T, Default> = T extends undefined ? Default : T;
+
+/**
+ * Zips a key tuple type and a value tuple type together into an object.
+ *
+ * @template Keys Keys of the resulting object.
+ * @template Values Values of the resulting object. If a key corresponds to a `Values` member that
+ * is undefined or past the end, it defaults to the corresponding `Defaults` member.
+ * @template Defaults Default values. If a key corresponds to a `Defaults` member that is past the
+ * end, the default falls back to `undefined`.
+ */
+export type ZipKeysWithValues<
+ Keys extends readonly string[],
+ Values extends readonly unknown[],
+ Defaults extends readonly unknown[]
+> =
+ //
+ Keys extends readonly [infer KHead, ...infer KTail]
+ ? {
+ readonly [k in EnsureSubtype<KHead, string>]: TypeOr<
+ TupleHeadOr<Values, undefined>,
+ TupleHeadOr<Defaults, undefined>
+ >;
+ } &
+ ZipKeysWithValues<
+ EnsureSubtype<KTail, readonly string[]>,
+ TupleTailOr<Values, []>,
+ TupleTailOr<Defaults, []>
+ >
+ : {}; // K exhausted
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts
new file mode 100644
index 0000000000..f775c3c634
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts
@@ -0,0 +1,303 @@
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { Logger } from '../internal/logging/logger.js';
+
+import { keysOf } from './data_tables.js';
+import { timeout } from './timeout.js';
+
+/**
+ * Error with arbitrary `extra` data attached, for debugging.
+ * The extra data is omitted if not running the test in debug mode (`?debug=1`).
+ */
+export class ErrorWithExtra extends Error {
+ readonly extra: { [k: string]: unknown };
+
+ /**
+ * `extra` function is only called if in debug mode.
+ * If an `ErrorWithExtra` is passed, its message is used and its extras are passed through.
+ */
+ constructor(message: string, extra: () => {});
+ constructor(base: ErrorWithExtra, newExtra: () => {});
+ constructor(baseOrMessage: string | ErrorWithExtra, newExtra: () => {}) {
+ const message = typeof baseOrMessage === 'string' ? baseOrMessage : baseOrMessage.message;
+ super(message);
+
+ const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {};
+ this.extra = Logger.globalDebugMode
+ ? { ...oldExtras, ...newExtra() }
+ : { omitted: 'pass ?debug=1' };
+ }
+}
+
+/**
+ * Asserts `condition` is true. Otherwise, throws an `Error` with the provided message.
+ */
+export function assert(condition: boolean, msg?: string | (() => string)): asserts condition {
+ if (!condition) {
+ throw new Error(msg && (typeof msg === 'string' ? msg : msg()));
+ }
+}
+
+/** If the argument is an Error, throw it. Otherwise, pass it back. */
+export function assertOK<T>(value: Error | T): T {
+ if (value instanceof Error) {
+ throw value;
+ }
+ return value;
+}
+
+/**
+ * Resolves if the provided promise rejects; rejects if it does not.
+ */
+export async function assertReject(p: Promise<unknown>, msg?: string): Promise<void> {
+ try {
+ await p;
+ unreachable(msg);
+ } catch (ex) {
+ // Assertion OK
+ }
+}
+
+/**
+ * Assert this code is unreachable. Unconditionally throws an `Error`.
+ */
+export function unreachable(msg?: string): never {
+ throw new Error(msg);
+}
+
+/**
+ * The `performance` interface.
+ * It is available in all browsers, but it is not in scope by default in Node.
+ */
+const perf = typeof performance !== 'undefined' ? performance : require('perf_hooks').performance;
+
+/**
+ * Calls the appropriate `performance.now()` depending on whether running in a browser or Node.
+ */
+export function now(): number {
+ return perf.now();
+}
+
+/**
+ * Returns a promise which resolves after the specified time.
+ */
+export function resolveOnTimeout(ms: number): Promise<void> {
+ return new Promise(resolve => {
+ timeout(() => {
+ resolve();
+ }, ms);
+ });
+}
+
+export class PromiseTimeoutError extends Error {}
+
+/**
+ * Returns a promise which rejects after the specified time.
+ */
+export function rejectOnTimeout(ms: number, msg: string): Promise<never> {
+ return new Promise((_resolve, reject) => {
+ timeout(() => {
+ reject(new PromiseTimeoutError(msg));
+ }, ms);
+ });
+}
+
+/**
+ * Takes a promise `p`, and returns a new one which rejects if `p` takes too long,
+ * and otherwise passes the result through.
+ */
+export function raceWithRejectOnTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
+ if (globalTestConfig.noRaceWithRejectOnTimeout) {
+ return p;
+ }
+ // Setup a promise that will reject after `ms` milliseconds. We cancel this timeout when
+ // `p` is finalized, so the JavaScript VM doesn't hang around waiting for the timer to
+ // complete, once the test runner has finished executing the tests.
+ const timeoutPromise = new Promise((_resolve, reject) => {
+ const handle = timeout(() => {
+ reject(new PromiseTimeoutError(msg));
+ }, ms);
+ p = p.finally(() => clearTimeout(handle));
+ });
+ return Promise.race([p, timeoutPromise]) as Promise<T>;
+}
+
+/**
+ * Takes a promise `p` and returns a new one which rejects if `p` resolves or rejects,
+ * and otherwise resolves after the specified time.
+ */
+export function assertNotSettledWithinTime(
+ p: Promise<unknown>,
+ ms: number,
+ msg: string
+): Promise<undefined> {
+ // Rejects regardless of whether p resolves or rejects.
+ const rejectWhenSettled = p.then(() => Promise.reject(new Error(msg)));
+ // Resolves after `ms` milliseconds.
+ const timeoutPromise = new Promise<undefined>(resolve => {
+ const handle = timeout(() => {
+ resolve(undefined);
+ }, ms);
+ p.finally(() => clearTimeout(handle));
+ });
+ return Promise.race([rejectWhenSettled, timeoutPromise]);
+}
+
+/**
+ * Returns a `Promise.reject()`, but also registers a dummy `.catch()` handler so it doesn't count
+ * as an uncaught promise rejection in the runtime.
+ */
+export function rejectWithoutUncaught<T>(err: unknown): Promise<T> {
+ const p = Promise.reject(err);
+ // Suppress uncaught promise rejection.
+ p.catch(() => {});
+ return p;
+}
+
+/**
+ * Makes a copy of a JS `object`, with the keys reordered into sorted order.
+ */
+export function sortObjectByKey(v: { [k: string]: unknown }): { [k: string]: unknown } {
+ const sortedObject: { [k: string]: unknown } = {};
+ for (const k of Object.keys(v).sort()) {
+ sortedObject[k] = v[k];
+ }
+ return sortedObject;
+}
+
+/**
+ * Determines whether two JS values are equal, recursing into objects and arrays.
+ * NaN is treated specially, such that `objectEquals(NaN, NaN)`.
+ */
+export function objectEquals(x: unknown, y: unknown): boolean {
+ if (typeof x !== 'object' || typeof y !== 'object') {
+ if (typeof x === 'number' && typeof y === 'number' && Number.isNaN(x) && Number.isNaN(y)) {
+ return true;
+ }
+ return x === y;
+ }
+ if (x === null || y === null) return x === y;
+ if (x.constructor !== y.constructor) return false;
+ if (x instanceof Function) return x === y;
+ if (x instanceof RegExp) return x === y;
+ if (x === y || x.valueOf() === y.valueOf()) return true;
+ if (Array.isArray(x) && Array.isArray(y) && x.length !== y.length) return false;
+ if (x instanceof Date) return false;
+ if (!(x instanceof Object)) return false;
+ if (!(y instanceof Object)) return false;
+
+ const x1 = x as { [k: string]: unknown };
+ const y1 = y as { [k: string]: unknown };
+ const p = Object.keys(x);
+ return Object.keys(y).every(i => p.indexOf(i) !== -1) && p.every(i => objectEquals(x1[i], y1[i]));
+}
+
+/**
+ * Generates a range of values `fn(0)..fn(n-1)`.
+ */
+export function range<T>(n: number, fn: (i: number) => T): T[] {
+ return [...new Array(n)].map((_, i) => fn(i));
+}
+
+/**
+ * Generates a range of values `fn(0)..fn(n-1)`.
+ */
+export function* iterRange<T>(n: number, fn: (i: number) => T): Iterable<T> {
+ for (let i = 0; i < n; ++i) {
+ yield fn(i);
+ }
+}
+
+/** Creates a (reusable) iterable object that maps `f` over `xs`, lazily. */
+export function mapLazy<T, R>(xs: Iterable<T>, f: (x: T) => R): Iterable<R> {
+ return {
+ *[Symbol.iterator]() {
+ for (const x of xs) {
+ yield f(x);
+ }
+ },
+ };
+}
+
+const TypedArrayBufferViewInstances = [
+ new Uint8Array(),
+ new Uint8ClampedArray(),
+ new Uint16Array(),
+ new Uint32Array(),
+ new Int8Array(),
+ new Int16Array(),
+ new Int32Array(),
+ new Float16Array(),
+ new Float32Array(),
+ new Float64Array(),
+] as const;
+
+export type TypedArrayBufferView = typeof TypedArrayBufferViewInstances[number];
+
+export type TypedArrayBufferViewConstructor<
+ A extends TypedArrayBufferView = TypedArrayBufferView
+> = {
+ // Interface copied from Uint8Array, and made generic.
+ readonly prototype: A;
+ readonly BYTES_PER_ELEMENT: number;
+
+ new (): A;
+ new (elements: Iterable<number>): A;
+ new (array: ArrayLike<number> | ArrayBufferLike): A;
+ new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): A;
+ new (length: number): A;
+
+ from(arrayLike: ArrayLike<number>): A;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ from(arrayLike: Iterable<number>, mapfn?: (v: number, k: number) => number, thisArg?: any): A;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): A;
+ of(...items: number[]): A;
+};
+
+export const kTypedArrayBufferViews: {
+ readonly [k: string]: TypedArrayBufferViewConstructor;
+} = {
+ ...(() => {
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ const result: { [k: string]: any } = {};
+ for (const v of TypedArrayBufferViewInstances) {
+ result[v.constructor.name] = v.constructor;
+ }
+ return result;
+ })(),
+};
+export const kTypedArrayBufferViewKeys = keysOf(kTypedArrayBufferViews);
+export const kTypedArrayBufferViewConstructors = Object.values(kTypedArrayBufferViews);
+
+function subarrayAsU8(
+ buf: ArrayBuffer | TypedArrayBufferView,
+ { start = 0, length }: { start?: number; length?: number }
+): Uint8Array | Uint8ClampedArray {
+ if (buf instanceof ArrayBuffer) {
+ return new Uint8Array(buf, start, length);
+ } else if (buf instanceof Uint8Array || buf instanceof Uint8ClampedArray) {
+ // Don't wrap in new views if we don't need to.
+ if (start === 0 && (length === undefined || length === buf.byteLength)) {
+ return buf;
+ }
+ }
+ const byteOffset = buf.byteOffset + start * buf.BYTES_PER_ELEMENT;
+ const byteLength =
+ length !== undefined
+ ? length * buf.BYTES_PER_ELEMENT
+ : buf.byteLength - (byteOffset - buf.byteOffset);
+ return new Uint8Array(buf.buffer, byteOffset, byteLength);
+}
+
+/**
+ * Copy a range of bytes from one ArrayBuffer or TypedArray to another.
+ *
+ * `start`/`length` are in elements (or in bytes, if ArrayBuffer).
+ */
+export function memcpy(
+ src: { src: ArrayBuffer | TypedArrayBufferView; start?: number; length?: number },
+ dst: { dst: ArrayBuffer | TypedArrayBufferView; start?: number }
+): void {
+ subarrayAsU8(dst.dst, dst).set(subarrayAsU8(src.src, src));
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts b/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts
new file mode 100644
index 0000000000..7d10520bcb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts
@@ -0,0 +1,24 @@
+import { timeout } from './timeout.js';
+
+// Copied from https://github.com/web-platform-tests/wpt/blob/master/common/reftest-wait.js
+
+/**
+ * Remove the `reftest-wait` class on the document element.
+ * The reftest runner will wait with taking a screenshot while
+ * this class is present.
+ *
+ * See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs
+ */
+export function takeScreenshot() {
+ document.documentElement.classList.remove('reftest-wait');
+}
+
+/**
+ * Call `takeScreenshot()` after a delay of at least `ms` milliseconds.
+ * @param {number} ms - milliseconds
+ */
+export function takeScreenshotDelayed(ms: number) {
+ timeout(() => {
+ takeScreenshot();
+ }, ms);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/README.txt b/dom/webgpu/tests/cts/checkout/src/demo/README.txt
new file mode 100644
index 0000000000..3b5654080e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/README.txt
@@ -0,0 +1 @@
+Demo test suite for manually testing test runners.
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/a.spec.ts
new file mode 100644
index 0000000000..283d0e8a90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a.spec.ts
@@ -0,0 +1,8 @@
+export const description = 'Description for a.spec.ts';
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { UnitTest } from '../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('not_implemented_yet').unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a/README.txt b/dom/webgpu/tests/cts/checkout/src/demo/a/README.txt
new file mode 100644
index 0000000000..62c18e3cc3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a/README.txt
@@ -0,0 +1 @@
+README for a/
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a/b.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/a/b.spec.ts
new file mode 100644
index 0000000000..7e066591dd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a/b.spec.ts
@@ -0,0 +1,6 @@
+export const description = 'Description for b.spec.ts';
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { UnitTest } from '../../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a/b/README.txt b/dom/webgpu/tests/cts/checkout/src/demo/a/b/README.txt
new file mode 100644
index 0000000000..eed2f44bbd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a/b/README.txt
@@ -0,0 +1 @@
+README for a/b/
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a/b/c.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/a/b/c.spec.ts
new file mode 100644
index 0000000000..0ee8f4c182
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a/b/c.spec.ts
@@ -0,0 +1,80 @@
+export const description = 'Description for c.spec.ts';
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { unreachable } from '../../../common/util/util.js';
+import { UnitTest } from '../../../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('f')
+ .desc(
+ `Test plan for f
+ - Test stuff
+ - Test some more stuff`
+ )
+ .fn(() => {});
+
+g.test('f,g').fn(() => {});
+
+g.test('f,g,h')
+ .paramsSimple([{}, { x: 0 }, { x: 0, y: 0 }])
+ .fn(() => {});
+
+g.test('case_depth_2_in_single_child_test')
+ .paramsSimple([{ x: 0, y: 0 }])
+ .fn(() => {});
+
+g.test('deep_case_tree')
+ .params(u =>
+ u //
+ .combine('x', [1, 2])
+ .combine('y', [1, 2])
+ .combine('z', [1, 2])
+ )
+ .fn(() => {});
+
+g.test('statuses,debug').fn(t => {
+ t.debug('debug');
+});
+
+g.test('statuses,skip').fn(t => {
+ t.skip('skip');
+});
+
+g.test('statuses,warn').fn(t => {
+ t.warn('warn');
+});
+
+g.test('statuses,fail').fn(t => {
+ t.fail('fail');
+});
+
+g.test('statuses,throw').fn(() => {
+ unreachable('unreachable');
+});
+
+g.test('multiple_same_stack').fn(t => {
+ for (let i = 0; i < 3; ++i) {
+ t.fail(
+ i === 2
+ ? 'this should appear after deduplicated line'
+ : 'this should be "seen 2 times with identical stack"'
+ );
+ }
+});
+
+g.test('multiple_same_level').fn(t => {
+ t.fail('this should print a stack');
+ t.fail('this should print a stack');
+ t.fail('this should not print a stack');
+});
+
+g.test('lower_levels_hidden,before').fn(t => {
+ t.warn('warn - this should not print a stack');
+ t.fail('fail');
+});
+
+g.test('lower_levels_hidden,after').fn(t => {
+ t.fail('fail');
+ t.warn('warn - this should not print a stack');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/a/b/d.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/a/b/d.spec.ts
new file mode 100644
index 0000000000..1412e53baf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/a/b/d.spec.ts
@@ -0,0 +1,8 @@
+export const description = 'Description for d.spec.ts';
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { UnitTest } from '../../../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('test_depth_2,in_single_child_file').fn(() => {});
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/file_depth_2/in_single_child_dir/r.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/file_depth_2/in_single_child_dir/r.spec.ts
new file mode 100644
index 0000000000..2a1adc6f50
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/file_depth_2/in_single_child_dir/r.spec.ts
@@ -0,0 +1,6 @@
+export const description = 'Description for r.spec.ts';
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { UnitTest } from '../../../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/json.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/json.spec.ts
new file mode 100644
index 0000000000..a2ccb72137
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/json.spec.ts
@@ -0,0 +1,10 @@
+export const description = 'Description for a.spec.ts';
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { UnitTest } from '../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('json')
+ .paramsSimple([{ p: { x: 1, y: 'two' } }])
+ .fn(() => {});
diff --git a/dom/webgpu/tests/cts/checkout/src/demo/subcases.spec.ts b/dom/webgpu/tests/cts/checkout/src/demo/subcases.spec.ts
new file mode 100644
index 0000000000..6b22463f07
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/demo/subcases.spec.ts
@@ -0,0 +1,38 @@
+export const description = 'Tests with subcases';
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { UnitTest } from '../unittests/unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('pass_warn_fail')
+ .params(u =>
+ u
+ .combine('x', [1, 2, 3]) //
+ .beginSubcases()
+ .combine('y', [1, 2, 3])
+ )
+ .fn(t => {
+ const { x, y } = t.params;
+ if (x + y > 5) {
+ t.fail();
+ } else if (x + y > 4) {
+ t.warn();
+ }
+ });
+
+g.test('DOMException,cases')
+ .params(u => u.combine('fail', [false, true]))
+ .fn(t => {
+ if (t.params.fail) {
+ throw new DOMException('Message!', 'Name!');
+ }
+ });
+
+g.test('DOMException,subcases')
+ .paramsSubcasesOnly(u => u.combine('fail', [false, true]))
+ .fn(t => {
+ if (t.params.fail) {
+ throw new DOMException('Message!', 'Name!');
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/external/README.md b/dom/webgpu/tests/cts/checkout/src/external/README.md
new file mode 100644
index 0000000000..84fbf9c732
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/external/README.md
@@ -0,0 +1,31 @@
+# External Modules
+
+This directory contains external modules that are used by the WebGPU
+CTS. These are included in the repo, as opposed to being fetched via a
+package manager or CDN, so that there is a single canonical source of
+truth for the CTS tests and the CTS tests can be run as a standalone
+suite without needing to pull from a CDN or similar process.
+
+## Adding modules
+
+Each module that is added should be done consciously with a clear
+reasoning on what the module is providing, since the bar for adding
+new modules should be relatively high.
+
+The module will need to be licensed via a compatible license to the
+BSD-3 clause & W3C CTS licenses that the CTS currently is covered by.
+
+It is preferred to use a single source build of the module if possible.
+
+In addition to the source for the module a LICENSE file should be
+included in the directory clearly identifying the owner of the module
+and the license it is covered by.
+
+Details of the specific module, including version, origin and purpose
+should be listed below.
+
+## Current Modules
+
+| **Name** | **Origin** | **License** | **Version** | **Purpose** |
+|----------------------|--------------------------------------------------|-------------|-------------|------------------------------------------------|
+| petamoriken/float16 | [github](https://github.com/petamoriken/float16) | MIT | 3.6.6 | Fluent support for f16 numbers via TypedArrays |
diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt
new file mode 100644
index 0000000000..e8eacf4e7f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-2021 Kenta Moriuchi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts
new file mode 100644
index 0000000000..c9d66ab7ca
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.d.ts
@@ -0,0 +1,471 @@
+/**
+ * A typed array of 16-bit float values. The contents are initialized to 0. If the requested number
+ * of bytes could not be allocated an exception is raised.
+ */
+export interface Float16Array {
+ /**
+ * The size in bytes of each element in the array.
+ */
+ readonly BYTES_PER_ELEMENT: number;
+
+ /**
+ * The ArrayBuffer instance referenced by the array.
+ */
+ readonly buffer: ArrayBufferLike;
+
+ /**
+ * The length in bytes of the array.
+ */
+ readonly byteLength: number;
+
+ /**
+ * The offset in bytes of the array.
+ */
+ readonly byteOffset: number;
+
+ [Symbol.iterator](): IterableIterator<number>;
+
+ /**
+ * Returns an array of key, value pairs for every entry in the array
+ */
+ entries(): IterableIterator<[number, number]>;
+
+ /**
+ * Returns an list of keys in the array
+ */
+ keys(): IterableIterator<number>;
+
+ /**
+ * Returns an list of values in the array
+ */
+ values(): IterableIterator<number>;
+
+ /**
+ * Returns the item located at the specified index.
+ * @param index The zero-based index of the desired code unit. A negative index will count back from the last item.
+ */
+ at(index: number): number | undefined;
+
+ /**
+ * Returns the this object after copying a section of the array identified by start and end
+ * to the same array starting at position target
+ * @param target If target is negative, it is treated as length+target where length is the
+ * length of the array.
+ * @param start If start is negative, it is treated as length+start. If end is negative, it
+ * is treated as length+end.
+ * @param end If not specified, length of the this object is used as its default value.
+ */
+ copyWithin(target: number, start: number, end?: number): this;
+
+ /**
+ * Determines whether all the members of an array satisfy the specified test.
+ * @param callbackfn A function that accepts up to three arguments. The every method calls
+ * the callbackfn function for each element in the array until the callbackfn returns a value
+ * which is coercible to the Boolean value false, or until the end of the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ every(
+ callbackfn: (value: number, index: number, array: Float16Array) => unknown,
+ thisArg?: any,
+ ): boolean;
+
+ /**
+ * Returns the this object after filling the section identified by start and end with value
+ * @param value value to fill array section with
+ * @param start index to start filling the array at. If start is negative, it is treated as
+ * length+start where length is the length of the array.
+ * @param end index to stop filling the array at. If end is negative, it is treated as
+ * length+end.
+ */
+ fill(value: number, start?: number, end?: number): this;
+
+ /**
+ * Returns the elements of an array that meet the condition specified in a callback function.
+ * @param predicate A function that accepts up to three arguments. The filter method calls
+ * the predicate function one time for each element in the array.
+ * @param thisArg An object to which the this keyword can refer in the predicate function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ filter(
+ predicate: (value: number, index: number, array: Float16Array) => any,
+ thisArg?: any,
+ ): Float16Array;
+
+ /**
+ * Returns the value of the first element in the array where predicate is true, and undefined
+ * otherwise.
+ * @param predicate find calls predicate once for each element of the array, in ascending
+ * order, until it finds one where predicate returns true. If such an element is found, find
+ * immediately returns that element value. Otherwise, find returns undefined.
+ * @param thisArg If provided, it will be used as the this value for each invocation of
+ * predicate. If it is not provided, undefined is used instead.
+ */
+ find(
+ predicate: (value: number, index: number, obj: Float16Array) => boolean,
+ thisArg?: any,
+ ): number | undefined;
+
+ /**
+ * Returns the index of the first element in the array where predicate is true, and -1
+ * otherwise.
+ * @param predicate find calls predicate once for each element of the array, in ascending
+ * order, until it finds one where predicate returns true. If such an element is found,
+ * findIndex immediately returns that element index. Otherwise, findIndex returns -1.
+ * @param thisArg If provided, it will be used as the this value for each invocation of
+ * predicate. If it is not provided, undefined is used instead.
+ */
+ findIndex(
+ predicate: (value: number, index: number, obj: Float16Array) => boolean,
+ thisArg?: any,
+ ): number;
+
+ /**
+ * Returns the value of the last element in the array where predicate is true, and undefined
+ * otherwise.
+ * @param predicate find calls predicate once for each element of the array, in descending
+ * order, until it finds one where predicate returns true. If such an element is found, findLast
+ * immediately returns that element value. Otherwise, findLast returns undefined.
+ * @param thisArg If provided, it will be used as the this value for each invocation of
+ * predicate. If it is not provided, undefined is used instead.
+ */
+ findLast(
+ predicate: (value: number, index: number, obj: Float16Array) => boolean,
+ thisArg?: any,
+ ): number | undefined;
+
+ /**
+ * Returns the index of the last element in the array where predicate is true, and -1
+ * otherwise.
+ * @param predicate find calls predicate once for each element of the array, in descending
+ * order, until it finds one where predicate returns true. If such an element is found,
+ * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1.
+ * @param thisArg If provided, it will be used as the this value for each invocation of
+ * predicate. If it is not provided, undefined is used instead.
+ */
+ findLastIndex(
+ predicate: (value: number, index: number, obj: Float16Array) => boolean,
+ thisArg?: any,
+ ): number;
+
+ /**
+ * Performs the specified action for each element in an array.
+ * @param callbackfn A function that accepts up to three arguments. forEach calls the
+ * callbackfn function one time for each element in the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ forEach(
+ callbackfn: (value: number, index: number, array: Float16Array) => void,
+ thisArg?: any,
+ ): void;
+
+ /**
+ * Determines whether an array includes a certain element, returning true or false as appropriate.
+ * @param searchElement The element to search for.
+ * @param fromIndex The position in this array at which to begin searching for searchElement.
+ */
+ includes(searchElement: number, fromIndex?: number): boolean;
+
+ /**
+ * Returns the index of the first occurrence of a value in an array.
+ * @param searchElement The value to locate in the array.
+ * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the
+ * search starts at index 0.
+ */
+ indexOf(searchElement: number, fromIndex?: number): number;
+
+ /**
+ * Adds all the elements of an array separated by the specified separator string.
+ * @param separator A string used to separate one element of an array from the next in the
+ * resulting String. If omitted, the array elements are separated with a comma.
+ */
+ join(separator?: string): string;
+
+ /**
+ * Returns the index of the last occurrence of a value in an array.
+ * @param searchElement The value to locate in the array.
+ * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the
+ * search starts at index 0.
+ */
+ lastIndexOf(searchElement: number, fromIndex?: number): number;
+
+ /**
+ * The length of the array.
+ */
+ readonly length: number;
+
+ /**
+ * Calls a defined callback function on each element of an array, and returns an array that
+ * contains the results.
+ * @param callbackfn A function that accepts up to three arguments. The map method calls the
+ * callbackfn function one time for each element in the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ map(
+ callbackfn: (value: number, index: number, array: Float16Array) => number,
+ thisArg?: any,
+ ): Float16Array;
+
+ /**
+ * Calls the specified callback function for all the elements in an array. The return value of
+ * the callback function is the accumulated result, and is provided as an argument in the next
+ * call to the callback function.
+ * @param callbackfn A function that accepts up to four arguments. The reduce method calls the
+ * callbackfn function one time for each element in the array.
+ * @param initialValue If initialValue is specified, it is used as the initial value to start
+ * the accumulation. The first call to the callbackfn function provides this value as an argument
+ * instead of an array value.
+ */
+ reduce(
+ callbackfn: (
+ previousValue: number,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => number,
+ ): number;
+ reduce(
+ callbackfn: (
+ previousValue: number,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => number,
+ initialValue: number,
+ ): number;
+ reduce<U>(
+ callbackfn: (
+ previousValue: U,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => U,
+ initialValue: U,
+ ): U;
+
+ /**
+ * Calls the specified callback function for all the elements in an array, in descending order.
+ * The return value of the callback function is the accumulated result, and is provided as an
+ * argument in the next call to the callback function.
+ * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls
+ * the callbackfn function one time for each element in the array.
+ * @param initialValue If initialValue is specified, it is used as the initial value to start
+ * the accumulation. The first call to the callbackfn function provides this value as an
+ * argument instead of an array value.
+ */
+ reduceRight(
+ callbackfn: (
+ previousValue: number,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => number,
+ ): number;
+ reduceRight(
+ callbackfn: (
+ previousValue: number,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => number,
+ initialValue: number,
+ ): number;
+ reduceRight<U>(
+ callbackfn: (
+ previousValue: U,
+ currentValue: number,
+ currentIndex: number,
+ array: Float16Array,
+ ) => U,
+ initialValue: U,
+ ): U;
+
+ /**
+ * Reverses the elements in an Array.
+ */
+ reverse(): this;
+
+ /**
+ * Sets a value or an array of values.
+ * @param array A typed or untyped array of values to set.
+ * @param offset The index in the current array at which the values are to be written.
+ */
+ set(array: ArrayLike<number>, offset?: number): void;
+
+ /**
+ * Returns a section of an array.
+ * @param start The beginning of the specified portion of the array.
+ * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'.
+ */
+ slice(start?: number, end?: number): Float16Array;
+
+ /**
+ * Determines whether the specified callback function returns true for any element of an array.
+ * @param callbackfn A function that accepts up to three arguments. The some method calls
+ * the callbackfn function for each element in the array until the callbackfn returns a value
+ * which is coercible to the Boolean value true, or until the end of the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ some(
+ callbackfn: (value: number, index: number, array: Float16Array) => unknown,
+ thisArg?: any,
+ ): boolean;
+
+ /**
+ * Sorts an array.
+ * @param compareFn Function used to determine the order of the elements. It is expected to return
+ * a negative value if first argument is less than second argument, zero if they're equal and a positive
+ * value otherwise. If omitted, the elements are sorted in ascending.
+ */
+ sort(compareFn?: (a: number, b: number) => number): this;
+
+ /**
+ * Gets a new Float16Array view of the ArrayBuffer store for this array, referencing the elements
+ * at begin, inclusive, up to end, exclusive.
+ * @param begin The index of the beginning of the array.
+ * @param end The index of the end of the array.
+ */
+ subarray(begin?: number, end?: number): Float16Array;
+
+ /**
+ * Converts a number to a string by using the current locale.
+ */
+ toLocaleString(): string;
+
+ /**
+ * Returns a string representation of an array.
+ */
+ toString(): string;
+
+ /**
+ * Returns the primitive value of the specified object.
+ */
+ valueOf(): Float16Array;
+
+ readonly [Symbol.toStringTag]: "Float16Array";
+
+ [index: number]: number;
+}
+
+export interface Float16ArrayConstructor {
+ readonly prototype: Float16Array;
+ new (): Float16Array;
+ new (length: number): Float16Array;
+ new (elements: Iterable<number>): Float16Array;
+ new (array: ArrayLike<number> | ArrayBufferLike): Float16Array;
+ new (
+ buffer: ArrayBufferLike,
+ byteOffset: number,
+ length?: number,
+ ): Float16Array;
+
+ /**
+ * The size in bytes of each element in the array.
+ */
+ readonly BYTES_PER_ELEMENT: number;
+
+ /**
+ * Returns a new array from a set of elements.
+ * @param items A set of elements to include in the new array object.
+ */
+ of(...items: number[]): Float16Array;
+
+ /**
+ * Creates an array from an array-like or iterable object.
+ * @param elements An iterable object to convert to an array.
+ */
+ from(elements: Iterable<number>): Float16Array;
+
+ /**
+ * Creates an array from an array-like or iterable object.
+ * @param elements An iterable object to convert to an array.
+ * @param mapfn A mapping function to call on every element of the array.
+ * @param thisArg Value of 'this' used to invoke the mapfn.
+ */
+ from<T>(
+ elements: Iterable<T>,
+ mapfn: (v: T, k: number) => number,
+ thisArg?: any,
+ ): Float16Array;
+
+ /**
+ * Creates an array from an array-like or iterable object.
+ * @param arrayLike An array-like object to convert to an array.
+ */
+ from(arrayLike: ArrayLike<number>): Float16Array;
+
+ /**
+ * Creates an array from an array-like or iterable object.
+ * @param arrayLike An array-like object to convert to an array.
+ * @param mapfn A mapping function to call on every element of the array.
+ * @param thisArg Value of 'this' used to invoke the mapfn.
+ */
+ from<T>(
+ arrayLike: ArrayLike<T>,
+ mapfn: (v: T, k: number) => number,
+ thisArg?: any,
+ ): Float16Array;
+}
+export declare const Float16Array: Float16ArrayConstructor;
+
+/**
+ * Returns `true` if the value is a Float16Array instance.
+ * @since v3.4.0
+ */
+export declare function isFloat16Array(value: unknown): value is Float16Array;
+
+/**
+ * Returns `true` if the value is a type of TypedArray instance that contains Float16Array.
+ * @since v3.6.0
+ */
+export declare function isTypedArray(
+ value: unknown,
+): value is
+ | Uint8Array
+ | Uint8ClampedArray
+ | Uint16Array
+ | Uint32Array
+ | Int8Array
+ | Int16Array
+ | Int32Array
+ | Float16Array
+ | Float32Array
+ | Float64Array
+ | BigUint64Array
+ | BigInt64Array;
+
+/**
+ * Gets the Float16 value at the specified byte offset from the start of the view. There is
+ * no alignment constraint; multi-byte values may be fetched from any offset.
+ * @param byteOffset The place in the buffer at which the value should be retrieved.
+ * @param littleEndian If false or undefined, a big-endian value should be read,
+ * otherwise a little-endian value should be read.
+ */
+export declare function getFloat16(
+ dataView: DataView,
+ byteOffset: number,
+ littleEndian?: boolean,
+): number;
+
+/**
+ * Stores an Float16 value at the specified byte offset from the start of the view.
+ * @param byteOffset The place in the buffer at which the value should be set.
+ * @param value The value to set.
+ * @param littleEndian If false or undefined, a big-endian value should be written,
+ * otherwise a little-endian value should be written.
+ */
+export declare function setFloat16(
+ dataView: DataView,
+ byteOffset: number,
+ value: number,
+ littleEndian?: boolean,
+): void;
+
+/**
+ * Returns the nearest half-precision float representation of a number.
+ * @param x A numeric expression.
+ */
+export declare function hfround(x: number): number;
diff --git a/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js
new file mode 100644
index 0000000000..54843a4842
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/external/petamoriken/float16/float16.js
@@ -0,0 +1,1228 @@
+/*! @petamoriken/float16 v3.6.6 | MIT License - https://github.com/petamoriken/float16 */
+
+const THIS_IS_NOT_AN_OBJECT = "This is not an object";
+const THIS_IS_NOT_A_FLOAT16ARRAY_OBJECT = "This is not a Float16Array object";
+const THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY =
+ "This constructor is not a subclass of Float16Array";
+const THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT =
+ "The constructor property value is not an object";
+const SPECIES_CONSTRUCTOR_DIDNT_RETURN_TYPEDARRAY_OBJECT =
+ "Species constructor didn't return TypedArray object";
+const DERIVED_CONSTRUCTOR_CREATED_TYPEDARRAY_OBJECT_WHICH_WAS_TOO_SMALL_LENGTH =
+ "Derived constructor created TypedArray object which was too small length";
+const ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER =
+ "Attempting to access detached ArrayBuffer";
+const CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT =
+ "Cannot convert undefined or null to object";
+const CANNOT_MIX_BIGINT_AND_OTHER_TYPES =
+ "Cannot mix BigInt and other types, use explicit conversions";
+const ITERATOR_PROPERTY_IS_NOT_CALLABLE = "@@iterator property is not callable";
+const REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE =
+ "Reduce of empty array with no initial value";
+const OFFSET_IS_OUT_OF_BOUNDS = "Offset is out of bounds";
+
+function uncurryThis(target) {
+ return (thisArg, ...args) => {
+ return ReflectApply(target, thisArg, args);
+ };
+}
+function uncurryThisGetter(target, key) {
+ return uncurryThis(
+ ReflectGetOwnPropertyDescriptor(
+ target,
+ key
+ ).get
+ );
+}
+const {
+ apply: ReflectApply,
+ construct: ReflectConstruct,
+ defineProperty: ReflectDefineProperty,
+ get: ReflectGet,
+ getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
+ getPrototypeOf: ReflectGetPrototypeOf,
+ has: ReflectHas,
+ ownKeys: ReflectOwnKeys,
+ set: ReflectSet,
+ setPrototypeOf: ReflectSetPrototypeOf,
+} = Reflect;
+const NativeProxy = Proxy;
+const {
+ MAX_SAFE_INTEGER: MAX_SAFE_INTEGER,
+ isFinite: NumberIsFinite,
+ isNaN: NumberIsNaN,
+} = Number;
+const {
+ iterator: SymbolIterator,
+ species: SymbolSpecies,
+ toStringTag: SymbolToStringTag,
+ for: SymbolFor,
+} = Symbol;
+const NativeObject = Object;
+const {
+ create: ObjectCreate,
+ defineProperty: ObjectDefineProperty,
+ freeze: ObjectFreeze,
+ is: ObjectIs,
+} = NativeObject;
+const ObjectPrototype = NativeObject.prototype;
+const ObjectPrototype__lookupGetter__ = (ObjectPrototype).__lookupGetter__
+ ? uncurryThis( (ObjectPrototype).__lookupGetter__)
+ : (object, key) => {
+ if (object == null) {
+ throw NativeTypeError(
+ CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT
+ );
+ }
+ let target = NativeObject(object);
+ do {
+ const descriptor = ReflectGetOwnPropertyDescriptor(target, key);
+ if (descriptor !== undefined) {
+ if (ObjectHasOwn(descriptor, "get")) {
+ return descriptor.get;
+ }
+ return;
+ }
+ } while ((target = ReflectGetPrototypeOf(target)) !== null);
+ };
+const ObjectHasOwn = (NativeObject).hasOwn ||
+ uncurryThis(ObjectPrototype.hasOwnProperty);
+const NativeArray = Array;
+const ArrayIsArray = NativeArray.isArray;
+const ArrayPrototype = NativeArray.prototype;
+const ArrayPrototypeJoin = uncurryThis(ArrayPrototype.join);
+const ArrayPrototypePush = uncurryThis(ArrayPrototype.push);
+const ArrayPrototypeToLocaleString = uncurryThis(
+ ArrayPrototype.toLocaleString
+);
+const NativeArrayPrototypeSymbolIterator = ArrayPrototype[SymbolIterator];
+const ArrayPrototypeSymbolIterator = uncurryThis(NativeArrayPrototypeSymbolIterator);
+const MathTrunc = Math.trunc;
+const NativeArrayBuffer = ArrayBuffer;
+const ArrayBufferIsView = NativeArrayBuffer.isView;
+const ArrayBufferPrototype = NativeArrayBuffer.prototype;
+const ArrayBufferPrototypeSlice = uncurryThis(ArrayBufferPrototype.slice);
+const ArrayBufferPrototypeGetByteLength = uncurryThisGetter(ArrayBufferPrototype, "byteLength");
+const NativeSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : null;
+const SharedArrayBufferPrototypeGetByteLength = NativeSharedArrayBuffer
+ && uncurryThisGetter(NativeSharedArrayBuffer.prototype, "byteLength");
+const TypedArray = ReflectGetPrototypeOf(Uint8Array);
+const TypedArrayFrom = TypedArray.from;
+const TypedArrayPrototype = TypedArray.prototype;
+const NativeTypedArrayPrototypeSymbolIterator = TypedArrayPrototype[SymbolIterator];
+const TypedArrayPrototypeKeys = uncurryThis(TypedArrayPrototype.keys);
+const TypedArrayPrototypeValues = uncurryThis(
+ TypedArrayPrototype.values
+);
+const TypedArrayPrototypeEntries = uncurryThis(
+ TypedArrayPrototype.entries
+);
+const TypedArrayPrototypeSet = uncurryThis(TypedArrayPrototype.set);
+const TypedArrayPrototypeReverse = uncurryThis(
+ TypedArrayPrototype.reverse
+);
+const TypedArrayPrototypeFill = uncurryThis(TypedArrayPrototype.fill);
+const TypedArrayPrototypeCopyWithin = uncurryThis(
+ TypedArrayPrototype.copyWithin
+);
+const TypedArrayPrototypeSort = uncurryThis(TypedArrayPrototype.sort);
+const TypedArrayPrototypeSlice = uncurryThis(TypedArrayPrototype.slice);
+const TypedArrayPrototypeSubarray = uncurryThis(
+ TypedArrayPrototype.subarray
+);
+const TypedArrayPrototypeGetBuffer = uncurryThisGetter(
+ TypedArrayPrototype,
+ "buffer"
+);
+const TypedArrayPrototypeGetByteOffset = uncurryThisGetter(
+ TypedArrayPrototype,
+ "byteOffset"
+);
+const TypedArrayPrototypeGetLength = uncurryThisGetter(
+ TypedArrayPrototype,
+ "length"
+);
+const TypedArrayPrototypeGetSymbolToStringTag = uncurryThisGetter(
+ TypedArrayPrototype,
+ SymbolToStringTag
+);
+const NativeUint16Array = Uint16Array;
+const Uint16ArrayFrom = (...args) => {
+ return ReflectApply(TypedArrayFrom, NativeUint16Array, args);
+};
+const NativeUint32Array = Uint32Array;
+const NativeFloat32Array = Float32Array;
+const ArrayIteratorPrototype = ReflectGetPrototypeOf([][SymbolIterator]());
+const ArrayIteratorPrototypeNext = uncurryThis(ArrayIteratorPrototype.next);
+const GeneratorPrototypeNext = uncurryThis((function* () {})().next);
+const IteratorPrototype = ReflectGetPrototypeOf(ArrayIteratorPrototype);
+const DataViewPrototype = DataView.prototype;
+const DataViewPrototypeGetUint16 = uncurryThis(
+ DataViewPrototype.getUint16
+);
+const DataViewPrototypeSetUint16 = uncurryThis(
+ DataViewPrototype.setUint16
+);
+const NativeTypeError = TypeError;
+const NativeRangeError = RangeError;
+const NativeWeakSet = WeakSet;
+const WeakSetPrototype = NativeWeakSet.prototype;
+const WeakSetPrototypeAdd = uncurryThis(WeakSetPrototype.add);
+const WeakSetPrototypeHas = uncurryThis(WeakSetPrototype.has);
+const NativeWeakMap = WeakMap;
+const WeakMapPrototype = NativeWeakMap.prototype;
+const WeakMapPrototypeGet = uncurryThis(WeakMapPrototype.get);
+const WeakMapPrototypeHas = uncurryThis(WeakMapPrototype.has);
+const WeakMapPrototypeSet = uncurryThis(WeakMapPrototype.set);
+
+const arrayIterators = new NativeWeakMap();
+const SafeIteratorPrototype = ObjectCreate(null, {
+ next: {
+ value: function next() {
+ const arrayIterator = WeakMapPrototypeGet(arrayIterators, this);
+ return ArrayIteratorPrototypeNext(arrayIterator);
+ },
+ },
+ [SymbolIterator]: {
+ value: function values() {
+ return this;
+ },
+ },
+});
+function safeIfNeeded(array) {
+ if (array[SymbolIterator] === NativeArrayPrototypeSymbolIterator) {
+ return array;
+ }
+ const safe = ObjectCreate(SafeIteratorPrototype);
+ WeakMapPrototypeSet(arrayIterators, safe, ArrayPrototypeSymbolIterator(array));
+ return safe;
+}
+const generators = new NativeWeakMap();
+const DummyArrayIteratorPrototype = ObjectCreate(IteratorPrototype, {
+ next: {
+ value: function next() {
+ const generator = WeakMapPrototypeGet(generators, this);
+ return GeneratorPrototypeNext(generator);
+ },
+ writable: true,
+ configurable: true,
+ },
+});
+for (const key of ReflectOwnKeys(ArrayIteratorPrototype)) {
+ if (key === "next") {
+ continue;
+ }
+ ObjectDefineProperty(DummyArrayIteratorPrototype, key, ReflectGetOwnPropertyDescriptor(ArrayIteratorPrototype, key));
+}
+function wrap(generator) {
+ const dummy = ObjectCreate(DummyArrayIteratorPrototype);
+ WeakMapPrototypeSet(generators, dummy, generator);
+ return dummy;
+}
+
+function isObject(value) {
+ return (value !== null && typeof value === "object") ||
+ typeof value === "function";
+}
+function isObjectLike(value) {
+ return value !== null && typeof value === "object";
+}
+function isNativeTypedArray(value) {
+ return TypedArrayPrototypeGetSymbolToStringTag(value) !== undefined;
+}
+function isNativeBigIntTypedArray(value) {
+ const typedArrayName = TypedArrayPrototypeGetSymbolToStringTag(value);
+ return typedArrayName === "BigInt64Array" ||
+ typedArrayName === "BigUint64Array";
+}
+function isArrayBuffer(value) {
+ try {
+ ArrayBufferPrototypeGetByteLength( (value));
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+function isSharedArrayBuffer(value) {
+ if (NativeSharedArrayBuffer === null) {
+ return false;
+ }
+ try {
+ SharedArrayBufferPrototypeGetByteLength( (value));
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+function isOrdinaryArray(value) {
+ if (!ArrayIsArray(value)) {
+ return false;
+ }
+ if (value[SymbolIterator] === NativeArrayPrototypeSymbolIterator) {
+ return true;
+ }
+ const iterator = value[SymbolIterator]();
+ return iterator[SymbolToStringTag] === "Array Iterator";
+}
+function isOrdinaryNativeTypedArray(value) {
+ if (!isNativeTypedArray(value)) {
+ return false;
+ }
+ if (value[SymbolIterator] === NativeTypedArrayPrototypeSymbolIterator) {
+ return true;
+ }
+ const iterator = value[SymbolIterator]();
+ return iterator[SymbolToStringTag] === "Array Iterator";
+}
+function isCanonicalIntegerIndexString(value) {
+ if (typeof value !== "string") {
+ return false;
+ }
+ const number = +value;
+ if (value !== number + "") {
+ return false;
+ }
+ if (!NumberIsFinite(number)) {
+ return false;
+ }
+ return number === MathTrunc(number);
+}
+
+const brand = SymbolFor("__Float16Array__");
+function hasFloat16ArrayBrand(target) {
+ if (!isObjectLike(target)) {
+ return false;
+ }
+ const prototype = ReflectGetPrototypeOf(target);
+ if (!isObjectLike(prototype)) {
+ return false;
+ }
+ const constructor = prototype.constructor;
+ if (constructor === undefined) {
+ return false;
+ }
+ if (!isObject(constructor)) {
+ throw NativeTypeError(THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT);
+ }
+ return ReflectHas(constructor, brand);
+}
+
+const buffer = new NativeArrayBuffer(4);
+const floatView = new NativeFloat32Array(buffer);
+const uint32View = new NativeUint32Array(buffer);
+const baseTable = new NativeUint32Array(512);
+const shiftTable = new NativeUint32Array(512);
+for (let i = 0; i < 256; ++i) {
+ const e = i - 127;
+ if (e < -27) {
+ baseTable[i] = 0x0000;
+ baseTable[i | 0x100] = 0x8000;
+ shiftTable[i] = 24;
+ shiftTable[i | 0x100] = 24;
+ } else if (e < -14) {
+ baseTable[i] = 0x0400 >> (-e - 14);
+ baseTable[i | 0x100] = (0x0400 >> (-e - 14)) | 0x8000;
+ shiftTable[i] = -e - 1;
+ shiftTable[i | 0x100] = -e - 1;
+ } else if (e <= 15) {
+ baseTable[i] = (e + 15) << 10;
+ baseTable[i | 0x100] = ((e + 15) << 10) | 0x8000;
+ shiftTable[i] = 13;
+ shiftTable[i | 0x100] = 13;
+ } else if (e < 128) {
+ baseTable[i] = 0x7c00;
+ baseTable[i | 0x100] = 0xfc00;
+ shiftTable[i] = 24;
+ shiftTable[i | 0x100] = 24;
+ } else {
+ baseTable[i] = 0x7c00;
+ baseTable[i | 0x100] = 0xfc00;
+ shiftTable[i] = 13;
+ shiftTable[i | 0x100] = 13;
+ }
+}
+function roundToFloat16Bits(num) {
+ floatView[0] = (num);
+ const f = uint32View[0];
+ const e = (f >> 23) & 0x1ff;
+ return baseTable[e] + ((f & 0x007fffff) >> shiftTable[e]);
+}
+const mantissaTable = new NativeUint32Array(2048);
+const exponentTable = new NativeUint32Array(64);
+const offsetTable = new NativeUint32Array(64);
+for (let i = 1; i < 1024; ++i) {
+ let m = i << 13;
+ let e = 0;
+ while((m & 0x00800000) === 0) {
+ m <<= 1;
+ e -= 0x00800000;
+ }
+ m &= ~0x00800000;
+ e += 0x38800000;
+ mantissaTable[i] = m | e;
+}
+for (let i = 1024; i < 2048; ++i) {
+ mantissaTable[i] = 0x38000000 + ((i - 1024) << 13);
+}
+for (let i = 1; i < 31; ++i) {
+ exponentTable[i] = i << 23;
+}
+exponentTable[31] = 0x47800000;
+exponentTable[32] = 0x80000000;
+for (let i = 33; i < 63; ++i) {
+ exponentTable[i] = 0x80000000 + ((i - 32) << 23);
+}
+exponentTable[63] = 0xc7800000;
+for (let i = 1; i < 64; ++i) {
+ if (i !== 32) {
+ offsetTable[i] = 1024;
+ }
+}
+function convertToNumber(float16bits) {
+ const m = float16bits >> 10;
+ uint32View[0] = mantissaTable[offsetTable[m] + (float16bits & 0x3ff)] + exponentTable[m];
+ return floatView[0];
+}
+
+function ToIntegerOrInfinity(target) {
+ const number = +target;
+ if (NumberIsNaN(number) || number === 0) {
+ return 0;
+ }
+ return MathTrunc(number);
+}
+function ToLength(target) {
+ const length = ToIntegerOrInfinity(target);
+ if (length < 0) {
+ return 0;
+ }
+ return length < MAX_SAFE_INTEGER
+ ? length
+ : MAX_SAFE_INTEGER;
+}
+function SpeciesConstructor(target, defaultConstructor) {
+ if (!isObject(target)) {
+ throw NativeTypeError(THIS_IS_NOT_AN_OBJECT);
+ }
+ const constructor = target.constructor;
+ if (constructor === undefined) {
+ return defaultConstructor;
+ }
+ if (!isObject(constructor)) {
+ throw NativeTypeError(THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT);
+ }
+ const species = constructor[SymbolSpecies];
+ if (species == null) {
+ return defaultConstructor;
+ }
+ return species;
+}
+function IsDetachedBuffer(buffer) {
+ if (isSharedArrayBuffer(buffer)) {
+ return false;
+ }
+ try {
+ ArrayBufferPrototypeSlice(buffer, 0, 0);
+ return false;
+ } catch (e) {}
+ return true;
+}
+function defaultCompare(x, y) {
+ const isXNaN = NumberIsNaN(x);
+ const isYNaN = NumberIsNaN(y);
+ if (isXNaN && isYNaN) {
+ return 0;
+ }
+ if (isXNaN) {
+ return 1;
+ }
+ if (isYNaN) {
+ return -1;
+ }
+ if (x < y) {
+ return -1;
+ }
+ if (x > y) {
+ return 1;
+ }
+ if (x === 0 && y === 0) {
+ const isXPlusZero = ObjectIs(x, 0);
+ const isYPlusZero = ObjectIs(y, 0);
+ if (!isXPlusZero && isYPlusZero) {
+ return -1;
+ }
+ if (isXPlusZero && !isYPlusZero) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+const BYTES_PER_ELEMENT = 2;
+const float16bitsArrays = new NativeWeakMap();
+function isFloat16Array(target) {
+ return WeakMapPrototypeHas(float16bitsArrays, target) ||
+ (!ArrayBufferIsView(target) && hasFloat16ArrayBrand(target));
+}
+function assertFloat16Array(target) {
+ if (!isFloat16Array(target)) {
+ throw NativeTypeError(THIS_IS_NOT_A_FLOAT16ARRAY_OBJECT);
+ }
+}
+function assertSpeciesTypedArray(target, count) {
+ const isTargetFloat16Array = isFloat16Array(target);
+ const isTargetTypedArray = isNativeTypedArray(target);
+ if (!isTargetFloat16Array && !isTargetTypedArray) {
+ throw NativeTypeError(SPECIES_CONSTRUCTOR_DIDNT_RETURN_TYPEDARRAY_OBJECT);
+ }
+ if (typeof count === "number") {
+ let length;
+ if (isTargetFloat16Array) {
+ const float16bitsArray = getFloat16BitsArray(target);
+ length = TypedArrayPrototypeGetLength(float16bitsArray);
+ } else {
+ length = TypedArrayPrototypeGetLength(target);
+ }
+ if (length < count) {
+ throw NativeTypeError(
+ DERIVED_CONSTRUCTOR_CREATED_TYPEDARRAY_OBJECT_WHICH_WAS_TOO_SMALL_LENGTH
+ );
+ }
+ }
+ if (isNativeBigIntTypedArray(target)) {
+ throw NativeTypeError(CANNOT_MIX_BIGINT_AND_OTHER_TYPES);
+ }
+}
+function getFloat16BitsArray(float16) {
+ const float16bitsArray = WeakMapPrototypeGet(float16bitsArrays, float16);
+ if (float16bitsArray !== undefined) {
+ const buffer = TypedArrayPrototypeGetBuffer(float16bitsArray);
+ if (IsDetachedBuffer(buffer)) {
+ throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER);
+ }
+ return float16bitsArray;
+ }
+ const buffer = (float16).buffer;
+ if (IsDetachedBuffer(buffer)) {
+ throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER);
+ }
+ const cloned = ReflectConstruct(Float16Array, [
+ buffer,
+ (float16).byteOffset,
+ (float16).length,
+ ], float16.constructor);
+ return WeakMapPrototypeGet(float16bitsArrays, cloned);
+}
+function copyToArray(float16bitsArray) {
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const array = [];
+ for (let i = 0; i < length; ++i) {
+ array[i] = convertToNumber(float16bitsArray[i]);
+ }
+ return array;
+}
+const TypedArrayPrototypeGetters = new NativeWeakSet();
+for (const key of ReflectOwnKeys(TypedArrayPrototype)) {
+ if (key === SymbolToStringTag) {
+ continue;
+ }
+ const descriptor = ReflectGetOwnPropertyDescriptor(TypedArrayPrototype, key);
+ if (ObjectHasOwn(descriptor, "get") && typeof descriptor.get === "function") {
+ WeakSetPrototypeAdd(TypedArrayPrototypeGetters, descriptor.get);
+ }
+}
+const handler = ObjectFreeze( ({
+ get(target, key, receiver) {
+ if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) {
+ return convertToNumber(ReflectGet(target, key));
+ }
+ if (WeakSetPrototypeHas(TypedArrayPrototypeGetters, ObjectPrototype__lookupGetter__(target, key))) {
+ return ReflectGet(target, key);
+ }
+ return ReflectGet(target, key, receiver);
+ },
+ set(target, key, value, receiver) {
+ if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) {
+ return ReflectSet(target, key, roundToFloat16Bits(value));
+ }
+ return ReflectSet(target, key, value, receiver);
+ },
+ getOwnPropertyDescriptor(target, key) {
+ if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) {
+ const descriptor = ReflectGetOwnPropertyDescriptor(target, key);
+ descriptor.value = convertToNumber(descriptor.value);
+ return descriptor;
+ }
+ return ReflectGetOwnPropertyDescriptor(target, key);
+ },
+ defineProperty(target, key, descriptor) {
+ if (
+ isCanonicalIntegerIndexString(key) &&
+ ObjectHasOwn(target, key) &&
+ ObjectHasOwn(descriptor, "value")
+ ) {
+ descriptor.value = roundToFloat16Bits(descriptor.value);
+ return ReflectDefineProperty(target, key, descriptor);
+ }
+ return ReflectDefineProperty(target, key, descriptor);
+ },
+}));
+class Float16Array {
+ constructor(input, _byteOffset, _length) {
+ let float16bitsArray;
+ if (isFloat16Array(input)) {
+ float16bitsArray = ReflectConstruct(NativeUint16Array, [getFloat16BitsArray(input)], new.target);
+ } else if (isObject(input) && !isArrayBuffer(input)) {
+ let list;
+ let length;
+ if (isNativeTypedArray(input)) {
+ list = input;
+ length = TypedArrayPrototypeGetLength(input);
+ const buffer = TypedArrayPrototypeGetBuffer(input);
+ const BufferConstructor = !isSharedArrayBuffer(buffer)
+ ? (SpeciesConstructor(
+ buffer,
+ NativeArrayBuffer
+ ))
+ : NativeArrayBuffer;
+ if (IsDetachedBuffer(buffer)) {
+ throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER);
+ }
+ if (isNativeBigIntTypedArray(input)) {
+ throw NativeTypeError(CANNOT_MIX_BIGINT_AND_OTHER_TYPES);
+ }
+ const data = new BufferConstructor(
+ length * BYTES_PER_ELEMENT
+ );
+ float16bitsArray = ReflectConstruct(NativeUint16Array, [data], new.target);
+ } else {
+ const iterator = input[SymbolIterator];
+ if (iterator != null && typeof iterator !== "function") {
+ throw NativeTypeError(ITERATOR_PROPERTY_IS_NOT_CALLABLE);
+ }
+ if (iterator != null) {
+ if (isOrdinaryArray(input)) {
+ list = input;
+ length = input.length;
+ } else {
+ list = [... (input)];
+ length = list.length;
+ }
+ } else {
+ list = (input);
+ length = ToLength(list.length);
+ }
+ float16bitsArray = ReflectConstruct(NativeUint16Array, [length], new.target);
+ }
+ for (let i = 0; i < length; ++i) {
+ float16bitsArray[i] = roundToFloat16Bits(list[i]);
+ }
+ } else {
+ float16bitsArray = ReflectConstruct(NativeUint16Array, arguments, new.target);
+ }
+ const proxy = (new NativeProxy(float16bitsArray, handler));
+ WeakMapPrototypeSet(float16bitsArrays, proxy, float16bitsArray);
+ return proxy;
+ }
+ static from(src, ...opts) {
+ const Constructor = this;
+ if (!ReflectHas(Constructor, brand)) {
+ throw NativeTypeError(
+ THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY
+ );
+ }
+ if (Constructor === Float16Array) {
+ if (isFloat16Array(src) && opts.length === 0) {
+ const float16bitsArray = getFloat16BitsArray(src);
+ const uint16 = new NativeUint16Array(
+ TypedArrayPrototypeGetBuffer(float16bitsArray),
+ TypedArrayPrototypeGetByteOffset(float16bitsArray),
+ TypedArrayPrototypeGetLength(float16bitsArray)
+ );
+ return new Float16Array(
+ TypedArrayPrototypeGetBuffer(TypedArrayPrototypeSlice(uint16))
+ );
+ }
+ if (opts.length === 0) {
+ return new Float16Array(
+ TypedArrayPrototypeGetBuffer(
+ Uint16ArrayFrom(src, roundToFloat16Bits)
+ )
+ );
+ }
+ const mapFunc = opts[0];
+ const thisArg = opts[1];
+ return new Float16Array(
+ TypedArrayPrototypeGetBuffer(
+ Uint16ArrayFrom(src, function (val, ...args) {
+ return roundToFloat16Bits(
+ ReflectApply(mapFunc, this, [val, ...safeIfNeeded(args)])
+ );
+ }, thisArg)
+ )
+ );
+ }
+ let list;
+ let length;
+ const iterator = src[SymbolIterator];
+ if (iterator != null && typeof iterator !== "function") {
+ throw NativeTypeError(ITERATOR_PROPERTY_IS_NOT_CALLABLE);
+ }
+ if (iterator != null) {
+ if (isOrdinaryArray(src)) {
+ list = src;
+ length = src.length;
+ } else if (isOrdinaryNativeTypedArray(src)) {
+ list = src;
+ length = TypedArrayPrototypeGetLength(src);
+ } else {
+ list = [...src];
+ length = list.length;
+ }
+ } else {
+ if (src == null) {
+ throw NativeTypeError(
+ CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT
+ );
+ }
+ list = NativeObject(src);
+ length = ToLength(list.length);
+ }
+ const array = new Constructor(length);
+ if (opts.length === 0) {
+ for (let i = 0; i < length; ++i) {
+ array[i] = (list[i]);
+ }
+ } else {
+ const mapFunc = opts[0];
+ const thisArg = opts[1];
+ for (let i = 0; i < length; ++i) {
+ array[i] = ReflectApply(mapFunc, thisArg, [list[i], i]);
+ }
+ }
+ return array;
+ }
+ static of(...items) {
+ const Constructor = this;
+ if (!ReflectHas(Constructor, brand)) {
+ throw NativeTypeError(
+ THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY
+ );
+ }
+ const length = items.length;
+ if (Constructor === Float16Array) {
+ const proxy = new Float16Array(length);
+ const float16bitsArray = getFloat16BitsArray(proxy);
+ for (let i = 0; i < length; ++i) {
+ float16bitsArray[i] = roundToFloat16Bits(items[i]);
+ }
+ return proxy;
+ }
+ const array = new Constructor(length);
+ for (let i = 0; i < length; ++i) {
+ array[i] = items[i];
+ }
+ return array;
+ }
+ keys() {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ return TypedArrayPrototypeKeys(float16bitsArray);
+ }
+ values() {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ return wrap((function* () {
+ for (const val of TypedArrayPrototypeValues(float16bitsArray)) {
+ yield convertToNumber(val);
+ }
+ })());
+ }
+ entries() {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ return wrap((function* () {
+ for (const [i, val] of TypedArrayPrototypeEntries(float16bitsArray)) {
+ yield ([i, convertToNumber(val)]);
+ }
+ })());
+ }
+ at(index) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const relativeIndex = ToIntegerOrInfinity(index);
+ const k = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;
+ if (k < 0 || k >= length) {
+ return;
+ }
+ return convertToNumber(float16bitsArray[k]);
+ }
+ map(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ const Constructor = SpeciesConstructor(float16bitsArray, Float16Array);
+ if (Constructor === Float16Array) {
+ const proxy = new Float16Array(length);
+ const array = getFloat16BitsArray(proxy);
+ for (let i = 0; i < length; ++i) {
+ const val = convertToNumber(float16bitsArray[i]);
+ array[i] = roundToFloat16Bits(
+ ReflectApply(callback, thisArg, [val, i, this])
+ );
+ }
+ return proxy;
+ }
+ const array = new Constructor(length);
+ assertSpeciesTypedArray(array, length);
+ for (let i = 0; i < length; ++i) {
+ const val = convertToNumber(float16bitsArray[i]);
+ array[i] = ReflectApply(callback, thisArg, [val, i, this]);
+ }
+ return (array);
+ }
+ filter(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ const kept = [];
+ for (let i = 0; i < length; ++i) {
+ const val = convertToNumber(float16bitsArray[i]);
+ if (ReflectApply(callback, thisArg, [val, i, this])) {
+ ArrayPrototypePush(kept, val);
+ }
+ }
+ const Constructor = SpeciesConstructor(float16bitsArray, Float16Array);
+ const array = new Constructor(kept);
+ assertSpeciesTypedArray(array);
+ return (array);
+ }
+ reduce(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ if (length === 0 && opts.length === 0) {
+ throw NativeTypeError(REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE);
+ }
+ let accumulator, start;
+ if (opts.length === 0) {
+ accumulator = convertToNumber(float16bitsArray[0]);
+ start = 1;
+ } else {
+ accumulator = opts[0];
+ start = 0;
+ }
+ for (let i = start; i < length; ++i) {
+ accumulator = callback(
+ accumulator,
+ convertToNumber(float16bitsArray[i]),
+ i,
+ this
+ );
+ }
+ return accumulator;
+ }
+ reduceRight(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ if (length === 0 && opts.length === 0) {
+ throw NativeTypeError(REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE);
+ }
+ let accumulator, start;
+ if (opts.length === 0) {
+ accumulator = convertToNumber(float16bitsArray[length - 1]);
+ start = length - 2;
+ } else {
+ accumulator = opts[0];
+ start = length - 1;
+ }
+ for (let i = start; i >= 0; --i) {
+ accumulator = callback(
+ accumulator,
+ convertToNumber(float16bitsArray[i]),
+ i,
+ this
+ );
+ }
+ return accumulator;
+ }
+ forEach(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = 0; i < length; ++i) {
+ ReflectApply(callback, thisArg, [
+ convertToNumber(float16bitsArray[i]),
+ i,
+ this,
+ ]);
+ }
+ }
+ find(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = 0; i < length; ++i) {
+ const value = convertToNumber(float16bitsArray[i]);
+ if (ReflectApply(callback, thisArg, [value, i, this])) {
+ return value;
+ }
+ }
+ }
+ findIndex(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = 0; i < length; ++i) {
+ const value = convertToNumber(float16bitsArray[i]);
+ if (ReflectApply(callback, thisArg, [value, i, this])) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ findLast(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = length - 1; i >= 0; --i) {
+ const value = convertToNumber(float16bitsArray[i]);
+ if (ReflectApply(callback, thisArg, [value, i, this])) {
+ return value;
+ }
+ }
+ }
+ findLastIndex(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = length - 1; i >= 0; --i) {
+ const value = convertToNumber(float16bitsArray[i]);
+ if (ReflectApply(callback, thisArg, [value, i, this])) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ every(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = 0; i < length; ++i) {
+ if (
+ !ReflectApply(callback, thisArg, [
+ convertToNumber(float16bitsArray[i]),
+ i,
+ this,
+ ])
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ some(callback, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const thisArg = opts[0];
+ for (let i = 0; i < length; ++i) {
+ if (
+ ReflectApply(callback, thisArg, [
+ convertToNumber(float16bitsArray[i]),
+ i,
+ this,
+ ])
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ set(input, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const targetOffset = ToIntegerOrInfinity(opts[0]);
+ if (targetOffset < 0) {
+ throw NativeRangeError(OFFSET_IS_OUT_OF_BOUNDS);
+ }
+ if (input == null) {
+ throw NativeTypeError(
+ CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT
+ );
+ }
+ if (isNativeBigIntTypedArray(input)) {
+ throw NativeTypeError(
+ CANNOT_MIX_BIGINT_AND_OTHER_TYPES
+ );
+ }
+ if (isFloat16Array(input)) {
+ return TypedArrayPrototypeSet(
+ getFloat16BitsArray(this),
+ getFloat16BitsArray(input),
+ targetOffset
+ );
+ }
+ if (isNativeTypedArray(input)) {
+ const buffer = TypedArrayPrototypeGetBuffer(input);
+ if (IsDetachedBuffer(buffer)) {
+ throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER);
+ }
+ }
+ const targetLength = TypedArrayPrototypeGetLength(float16bitsArray);
+ const src = NativeObject(input);
+ const srcLength = ToLength(src.length);
+ if (targetOffset === Infinity || srcLength + targetOffset > targetLength) {
+ throw NativeRangeError(OFFSET_IS_OUT_OF_BOUNDS);
+ }
+ for (let i = 0; i < srcLength; ++i) {
+ float16bitsArray[i + targetOffset] = roundToFloat16Bits(src[i]);
+ }
+ }
+ reverse() {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ TypedArrayPrototypeReverse(float16bitsArray);
+ return this;
+ }
+ fill(value, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ TypedArrayPrototypeFill(
+ float16bitsArray,
+ roundToFloat16Bits(value),
+ ...safeIfNeeded(opts)
+ );
+ return this;
+ }
+ copyWithin(target, start, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ TypedArrayPrototypeCopyWithin(float16bitsArray, target, start, ...safeIfNeeded(opts));
+ return this;
+ }
+ sort(compareFn) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const sortCompare = compareFn !== undefined ? compareFn : defaultCompare;
+ TypedArrayPrototypeSort(float16bitsArray, (x, y) => {
+ return sortCompare(convertToNumber(x), convertToNumber(y));
+ });
+ return this;
+ }
+ slice(start, end) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const Constructor = SpeciesConstructor(float16bitsArray, Float16Array);
+ if (Constructor === Float16Array) {
+ const uint16 = new NativeUint16Array(
+ TypedArrayPrototypeGetBuffer(float16bitsArray),
+ TypedArrayPrototypeGetByteOffset(float16bitsArray),
+ TypedArrayPrototypeGetLength(float16bitsArray)
+ );
+ return new Float16Array(
+ TypedArrayPrototypeGetBuffer(
+ TypedArrayPrototypeSlice(uint16, start, end)
+ )
+ );
+ }
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ const relativeStart = ToIntegerOrInfinity(start);
+ const relativeEnd = end === undefined ? length : ToIntegerOrInfinity(end);
+ let k;
+ if (relativeStart === -Infinity) {
+ k = 0;
+ } else if (relativeStart < 0) {
+ k = length + relativeStart > 0 ? length + relativeStart : 0;
+ } else {
+ k = length < relativeStart ? length : relativeStart;
+ }
+ let final;
+ if (relativeEnd === -Infinity) {
+ final = 0;
+ } else if (relativeEnd < 0) {
+ final = length + relativeEnd > 0 ? length + relativeEnd : 0;
+ } else {
+ final = length < relativeEnd ? length : relativeEnd;
+ }
+ const count = final - k > 0 ? final - k : 0;
+ const array = new Constructor(count);
+ assertSpeciesTypedArray(array, count);
+ if (count === 0) {
+ return array;
+ }
+ const buffer = TypedArrayPrototypeGetBuffer(float16bitsArray);
+ if (IsDetachedBuffer(buffer)) {
+ throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER);
+ }
+ let n = 0;
+ while (k < final) {
+ array[n] = convertToNumber(float16bitsArray[k]);
+ ++k;
+ ++n;
+ }
+ return (array);
+ }
+ subarray(begin, end) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const Constructor = SpeciesConstructor(float16bitsArray, Float16Array);
+ const uint16 = new NativeUint16Array(
+ TypedArrayPrototypeGetBuffer(float16bitsArray),
+ TypedArrayPrototypeGetByteOffset(float16bitsArray),
+ TypedArrayPrototypeGetLength(float16bitsArray)
+ );
+ const uint16Subarray = TypedArrayPrototypeSubarray(uint16, begin, end);
+ const array = new Constructor(
+ TypedArrayPrototypeGetBuffer(uint16Subarray),
+ TypedArrayPrototypeGetByteOffset(uint16Subarray),
+ TypedArrayPrototypeGetLength(uint16Subarray)
+ );
+ assertSpeciesTypedArray(array);
+ return (array);
+ }
+ indexOf(element, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ let from = ToIntegerOrInfinity(opts[0]);
+ if (from === Infinity) {
+ return -1;
+ }
+ if (from < 0) {
+ from += length;
+ if (from < 0) {
+ from = 0;
+ }
+ }
+ for (let i = from; i < length; ++i) {
+ if (
+ ObjectHasOwn(float16bitsArray, i) &&
+ convertToNumber(float16bitsArray[i]) === element
+ ) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ lastIndexOf(element, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ let from = opts.length >= 1 ? ToIntegerOrInfinity(opts[0]) : length - 1;
+ if (from === -Infinity) {
+ return -1;
+ }
+ if (from >= 0) {
+ from = from < length - 1 ? from : length - 1;
+ } else {
+ from += length;
+ }
+ for (let i = from; i >= 0; --i) {
+ if (
+ ObjectHasOwn(float16bitsArray, i) &&
+ convertToNumber(float16bitsArray[i]) === element
+ ) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ includes(element, ...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const length = TypedArrayPrototypeGetLength(float16bitsArray);
+ let from = ToIntegerOrInfinity(opts[0]);
+ if (from === Infinity) {
+ return false;
+ }
+ if (from < 0) {
+ from += length;
+ if (from < 0) {
+ from = 0;
+ }
+ }
+ const isNaN = NumberIsNaN(element);
+ for (let i = from; i < length; ++i) {
+ const value = convertToNumber(float16bitsArray[i]);
+ if (isNaN && NumberIsNaN(value)) {
+ return true;
+ }
+ if (value === element) {
+ return true;
+ }
+ }
+ return false;
+ }
+ join(separator) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const array = copyToArray(float16bitsArray);
+ return ArrayPrototypeJoin(array, separator);
+ }
+ toLocaleString(...opts) {
+ assertFloat16Array(this);
+ const float16bitsArray = getFloat16BitsArray(this);
+ const array = copyToArray(float16bitsArray);
+ return ArrayPrototypeToLocaleString(array, ...safeIfNeeded(opts));
+ }
+ get [SymbolToStringTag]() {
+ if (isFloat16Array(this)) {
+ return ("Float16Array");
+ }
+ }
+}
+ObjectDefineProperty(Float16Array, "BYTES_PER_ELEMENT", {
+ value: BYTES_PER_ELEMENT,
+});
+ObjectDefineProperty(Float16Array, brand, {});
+ReflectSetPrototypeOf(Float16Array, TypedArray);
+const Float16ArrayPrototype = Float16Array.prototype;
+ObjectDefineProperty(Float16ArrayPrototype, "BYTES_PER_ELEMENT", {
+ value: BYTES_PER_ELEMENT,
+});
+ObjectDefineProperty(Float16ArrayPrototype, SymbolIterator, {
+ value: Float16ArrayPrototype.values,
+ writable: true,
+ configurable: true,
+});
+ReflectSetPrototypeOf(Float16ArrayPrototype, TypedArrayPrototype);
+
+function isTypedArray(target) {
+ return isNativeTypedArray(target) || isFloat16Array(target);
+}
+
+function getFloat16(dataView, byteOffset, ...opts) {
+ return convertToNumber(
+ DataViewPrototypeGetUint16(dataView, byteOffset, ...safeIfNeeded(opts))
+ );
+}
+function setFloat16(dataView, byteOffset, value, ...opts) {
+ return DataViewPrototypeSetUint16(
+ dataView,
+ byteOffset,
+ roundToFloat16Bits(value),
+ ...safeIfNeeded(opts)
+ );
+}
+
+function hfround(x) {
+ const number = +x;
+ if (!NumberIsFinite(number) || number === 0) {
+ return number;
+ }
+ const x16 = roundToFloat16Bits(number);
+ return convertToNumber(x16);
+}
+
+export { Float16Array, getFloat16, hfround, isFloat16Array, isTypedArray, setFloat16 };
diff --git a/dom/webgpu/tests/cts/checkout/src/manual/README.txt b/dom/webgpu/tests/cts/checkout/src/manual/README.txt
new file mode 100644
index 0000000000..a50ded41db
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/manual/README.txt
@@ -0,0 +1,18 @@
+WebGPU tests that require manual intervention.
+
+Many of these test may be HTML pages rather than using the harness.
+
+Add informal notes here on possible stress tests.
+
+- Suspending or hibernating the machine.
+- Manually crashing or relaunching the browser's GPU process.
+- Triggering a GPU driver reset (TDR).
+- Forcibly or gracefully unplugging an external GPU.
+- Forcibly switching between GPUs using OS/driver settings.
+- Backgrounding the browser (on mobile OSes).
+- Moving windows between displays attached to different hardware adapters.
+- Moving windows between displays with different color properties (HDR/WCG).
+- Unplugging a laptop.
+- Switching between canvas and XR device output.
+
+TODO: look at dEQP (OpenGL ES and Vulkan) and WebGL for inspiration here.
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/README.md b/dom/webgpu/tests/cts/checkout/src/resources/README.md
new file mode 100644
index 0000000000..c918eb373c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/README.md
@@ -0,0 +1,2 @@
+Always use `getResourcePath()` to get the appropriate path to these resources depending
+on the context (WPT, standalone, worker, etc.)
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt2020.vp9.webm b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt2020.vp9.webm
new file mode 100644
index 0000000000..91850cdbea
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt2020.vp9.webm
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt601.vp9.webm b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt601.vp9.webm
new file mode 100644
index 0000000000..d90e1911c6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt601.vp9.webm
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt709.vp9.webm b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt709.vp9.webm
new file mode 100644
index 0000000000..1bb70ef572
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.bt709.vp9.webm
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.mp4 b/dom/webgpu/tests/cts/checkout/src/resources/red-green.mp4
new file mode 100644
index 0000000000..4bd6d59658
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.mp4
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.theora.ogv b/dom/webgpu/tests/cts/checkout/src/resources/red-green.theora.ogv
new file mode 100644
index 0000000000..1543915a10
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.theora.ogv
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/red-green.webmvp8.webm b/dom/webgpu/tests/cts/checkout/src/resources/red-green.webmvp8.webm
new file mode 100644
index 0000000000..fde59a18b4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/red-green.webmvp8.webm
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/resources/webgpu.png b/dom/webgpu/tests/cts/checkout/src/resources/webgpu.png
new file mode 100644
index 0000000000..eec0d6eb90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/resources/webgpu.png
Binary files differ
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/README.txt
new file mode 100644
index 0000000000..5457e8400d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/README.txt
@@ -0,0 +1,6 @@
+WebGPU stress tests.
+
+These tests are separated from conformance tests because they are more likely to
+cause browser hangs and crashes.
+
+TODO: Look at dEQP (OpenGL ES and Vulkan) and WebGL for inspiration here.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/adapter/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/adapter/README.txt
new file mode 100644
index 0000000000..3a57f3d87f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/adapter/README.txt
@@ -0,0 +1 @@
+Stress tests covering use of GPUAdapter.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/adapter/device_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/adapter/device_allocation.spec.ts
new file mode 100644
index 0000000000..184b4e8170
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/adapter/device_allocation.spec.ts
@@ -0,0 +1,290 @@
+export const description = `
+Stress tests for GPUAdapter.requestDevice.
+`;
+
+import { Fixture } from '../../common/framework/fixture.js';
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { attemptGarbageCollection } from '../../common/util/collect_garbage.js';
+import { keysOf } from '../../common/util/data_tables.js';
+import { getGPU } from '../../common/util/navigator_gpu.js';
+import { assert, iterRange } from '../../common/util/util.js';
+import { kLimitInfo } from '../../webgpu/capability_info.js';
+
+export const g = makeTestGroup(Fixture);
+
+/** Adapter preference identifier to option. */
+const kAdapterTypeOptions: {
+ readonly [k in GPUPowerPreference | 'fallback']: GPURequestAdapterOptions;
+} = /* prettier-ignore */ {
+ 'low-power': { powerPreference: 'low-power', forceFallbackAdapter: false },
+ 'high-performance': { powerPreference: 'high-performance', forceFallbackAdapter: false },
+ 'fallback': { powerPreference: undefined, forceFallbackAdapter: true },
+};
+/** List of all adapter hint types. */
+const kAdapterTypes = keysOf(kAdapterTypeOptions);
+
+/**
+ * Creates a device, a valid compute pipeline, valid resources for the pipeline, and
+ * ties them together into a set of compute commands ready to be submitted to the GPU
+ * queue. Does not submit the commands in order to make sure that all resources are
+ * kept alive until the device is destroyed.
+ */
+async function createDeviceAndComputeCommands(adapter: GPUAdapter) {
+ // Constants are computed such that per run, this function should allocate roughly 2G
+ // worth of data. This should be sufficient as we run these creation functions many
+ // times. If the data backing the created objects is not recycled we should OOM.
+ const kNumPipelines = 64;
+ const kNumBindgroups = 128;
+ const kNumBufferElements =
+ kLimitInfo.maxComputeWorkgroupSizeX.default * kLimitInfo.maxComputeWorkgroupSizeY.default;
+ const kBufferSize = kNumBufferElements * 4;
+ const kBufferData = new Uint32Array([...iterRange(kNumBufferElements, x => x)]);
+
+ const device: GPUDevice = await adapter.requestDevice();
+ const commands = [];
+
+ for (let pipelineIndex = 0; pipelineIndex < kNumPipelines; ++pipelineIndex) {
+ const pipeline = device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x * ${kLimitInfo.maxComputeWorkgroupSizeX.default}u + id.y] =
+ buffer.data[id.x * ${kLimitInfo.maxComputeWorkgroupSizeX.default}u + id.y] +
+ ${pipelineIndex}u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ for (let bindgroupIndex = 0; bindgroupIndex < kNumBindgroups; ++bindgroupIndex) {
+ const buffer = device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
+ });
+ device.queue.writeBuffer(buffer, 0, kBufferData, 0, kBufferData.length);
+ const bindgroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindgroup);
+ pass.dispatchWorkgroups(
+ kLimitInfo.maxComputeWorkgroupSizeX.default,
+ kLimitInfo.maxComputeWorkgroupSizeY.default
+ );
+ pass.end();
+ commands.push(encoder.finish());
+ }
+ }
+ return { device, objects: commands };
+}
+
+/**
+ * Creates a device, a valid render pipeline, valid resources for the pipeline, and
+ * ties them together into a set of render commands ready to be submitted to the GPU
+ * queue. Does not submit the commands in order to make sure that all resources are
+ * kept alive until the device is destroyed.
+ */
+async function createDeviceAndRenderCommands(adapter: GPUAdapter) {
+ // Constants are computed such that per run, this function should allocate roughly 2G
+ // worth of data. This should be sufficient as we run these creation functions many
+ // times. If the data backing the created objects is not recycled we should OOM.
+ const kNumPipelines = 128;
+ const kNumBindgroups = 128;
+ const kSize = 128;
+ const kBufferData = new Uint32Array([...iterRange(kSize * kSize, x => x)]);
+
+ const device: GPUDevice = await adapter.requestDevice();
+ const commands = [];
+
+ for (let pipelineIndex = 0; pipelineIndex < kNumPipelines; ++pipelineIndex) {
+ const module = device.createShaderModule({
+ code: `
+ struct Buffer { data: array<vec4<u32>, ${(kSize * kSize) / 4}>, };
+
+ @group(0) @binding(0) var<uniform> buffer: Buffer;
+ @vertex fn vmain(
+ @builtin(vertex_index) vertexIndex: u32
+ ) -> @builtin(position) vec4<f32> {
+ let index = buffer.data[vertexIndex / 4u][vertexIndex % 4u];
+ let position = vec2<f32>(f32(index % ${kSize}u), f32(index / ${kSize}u));
+ let r = vec2<f32>(1.0 / f32(${kSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(${pipelineIndex}.0 / ${kNumPipelines}.0, 0.0, 0.0, 1.0);
+ }
+ `,
+ });
+ const pipeline = device.createRenderPipeline({
+ layout: device.createPipelineLayout({
+ bindGroupLayouts: [
+ device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.VERTEX,
+ buffer: { type: 'uniform' },
+ },
+ ],
+ }),
+ ],
+ }),
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ for (let bindgroupIndex = 0; bindgroupIndex < kNumBindgroups; ++bindgroupIndex) {
+ const buffer = device.createBuffer({
+ size: kSize * kSize * 4,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ device.queue.writeBuffer(buffer, 0, kBufferData, 0, kBufferData.length);
+ const bindgroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ const texture = device.createTexture({
+ size: [kSize, kSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindgroup);
+ pass.draw(kSize * kSize);
+ pass.end();
+ commands.push(encoder.finish());
+ }
+ }
+ return { device, objects: commands };
+}
+
+/**
+ * Creates a device and a large number of buffers which are immediately written to. The
+ * buffers are expected to be kept alive until they or the device are destroyed.
+ */
+async function createDeviceAndBuffers(adapter: GPUAdapter) {
+ // Currently we just allocate 2G of memory using 512MB blocks. We may be able to
+ // increase this to hit OOM instead, but on integrated GPUs on Metal, this can cause
+ // kernel panics at the moment, and it can greatly increase the time needed.
+ const kTotalMemorySize = 2 * 1024 * 1024 * 1024;
+ const kMemoryBlockSize = 512 * 1024 * 1024;
+ const kMemoryBlockData = new Uint8Array(kMemoryBlockSize);
+
+ const device: GPUDevice = await adapter.requestDevice();
+ const buffers = [];
+ for (let memory = 0; memory < kTotalMemorySize; memory += kMemoryBlockSize) {
+ const buffer = device.createBuffer({
+ size: kMemoryBlockSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+
+ // Write out to the buffer to make sure that it has backing memory.
+ device.queue.writeBuffer(buffer, 0, kMemoryBlockData, 0, kMemoryBlockData.length);
+ buffers.push(buffer);
+ }
+ return { device, objects: buffers };
+}
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUDevice objects.`)
+ .params(u => u.combine('adapterType', kAdapterTypes))
+ .fn(async t => {
+ const { adapterType } = t.params;
+ const adapter = await getGPU().requestAdapter(kAdapterTypeOptions[adapterType]);
+ assert(adapter !== null, 'Failed to get adapter.');
+
+ // Based on Vulkan conformance test requirement to be able to create multiple devices.
+ const kNumDevices = 5;
+
+ const devices = [];
+ for (let i = 0; i < kNumDevices; ++i) {
+ const device: GPUDevice = await adapter.requestDevice();
+ devices.push(device);
+ }
+ });
+
+g.test('continuous,with_destroy')
+ .desc(
+ `Tests allocation and destruction of many GPUDevice objects over time. Device objects
+are sequentially requested with a series of device allocated objects created on each
+device. The devices are then destroyed to verify that the device and the device allocated
+objects are recycled over a very large number of iterations.`
+ )
+ .params(u => u.combine('adapterType', kAdapterTypes))
+ .fn(async t => {
+ const { adapterType } = t.params;
+ const adapter = await getGPU().requestAdapter(kAdapterTypeOptions[adapterType]);
+ assert(adapter !== null, 'Failed to get adapter.');
+
+ // Since devices are being destroyed, we should be able to create many devices.
+ const kNumDevices = 100;
+ const kFunctions = [
+ createDeviceAndBuffers,
+ createDeviceAndComputeCommands,
+ createDeviceAndRenderCommands,
+ ];
+
+ const deviceList = [];
+ const objectLists = [];
+ for (let i = 0; i < kNumDevices; ++i) {
+ const { device, objects } = await kFunctions[i % kFunctions.length](adapter);
+ t.expect(objects.length > 0, 'unable to allocate any objects');
+ deviceList.push(device);
+ objectLists.push(objects);
+ device.destroy();
+ }
+ });
+
+g.test('continuous,no_destroy')
+ .desc(
+ `Tests allocation and implicit GC of many GPUDevice objects over time. Objects are
+sequentially requested and dropped for GC over a very large number of iterations. Note
+that without destroy, we do not create device allocated objects because that will
+implicitly keep the device in scope.`
+ )
+ .params(u => u.combine('adapterType', kAdapterTypes))
+ .fn(async t => {
+ const { adapterType } = t.params;
+ const adapter = await getGPU().requestAdapter(kAdapterTypeOptions[adapterType]);
+ assert(adapter !== null, 'Failed to get adapter.');
+
+ const kNumDevices = 10_000;
+ for (let i = 1; i <= kNumDevices; ++i) {
+ await (async () => {
+ t.expect((await adapter.requestDevice()) !== null, 'unexpected null device');
+ })();
+ if (i % 10 === 0) {
+ // We need to occasionally wait for GC to clear out stale devices.
+ await attemptGarbageCollection();
+ }
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/compute/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/compute/README.txt
new file mode 100644
index 0000000000..b41aabc66b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/compute/README.txt
@@ -0,0 +1 @@
+Stress tests covering operations specific to GPUComputePipeline and GPUComputePass.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/compute/compute_pass.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/compute/compute_pass.spec.ts
new file mode 100644
index 0000000000..76979f9fbb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/compute/compute_pass.spec.ts
@@ -0,0 +1,243 @@
+export const description = `
+Stress tests covering GPUComputePassEncoder usage.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { assert, iterRange } from '../../common/util/util.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('many')
+ .desc(
+ `Tests execution of a huge number of compute passes using the same
+GPUComputePipeline.`
+ )
+ .fn(async t => {
+ const kNumElements = 64;
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = buffer.data[id.x] + 1u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ const kNumIterations = 250_000;
+ for (let i = 0; i < kNumIterations; ++i) {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+ t.expectGPUBufferValuesEqual(
+ buffer,
+ new Uint32Array([...iterRange(kNumElements, x => x + kNumIterations)])
+ );
+ });
+
+g.test('pipeline_churn')
+ .desc(
+ `Tests execution of a huge number of compute passes which each use a different
+GPUComputePipeline.`
+ )
+ .fn(async t => {
+ const buffer = t.makeBufferWithContents(
+ new Uint32Array([0]),
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
+ );
+ const kNumIterations = 10_000;
+ const stages = iterRange(kNumIterations, i => ({
+ module: t.device.createShaderModule({
+ code: `
+ struct Buffer { data: u32, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main${i}() {
+ buffer.data = buffer.data + 1u;
+ }
+ `,
+ }),
+ entryPoint: `main${i}`,
+ }));
+ for (const compute of stages) {
+ const encoder = t.device.createCommandEncoder();
+ const pipeline = t.device.createComputePipeline({ layout: 'auto', compute });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+ t.expectGPUBufferValuesEqual(buffer, new Uint32Array([kNumIterations]));
+ });
+
+g.test('bind_group_churn')
+ .desc(
+ `Tests execution of compute passes which switch between a huge number of bind
+groups.`
+ )
+ .fn(async t => {
+ const kNumElements = 64;
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer1 = t.makeBufferWithContents(
+ data,
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
+ );
+ const buffer2 = t.makeBufferWithContents(
+ data,
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
+ );
+ const module = t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer1: Buffer;
+ @group(0) @binding(1) var<storage, read_write> buffer2: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer1.data[id.x] = buffer1.data[id.x] + 1u;
+ buffer2.data[id.x] = buffer2.data[id.x] + 2u;
+ }
+ `,
+ });
+ const kNumIterations = 250_000;
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ for (let i = 0; i < kNumIterations; ++i) {
+ const buffer1Binding = i % 2;
+ const buffer2Binding = buffer1Binding ^ 1;
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: buffer1Binding, resource: { buffer: buffer1 } },
+ { binding: buffer2Binding, resource: { buffer: buffer2 } },
+ ],
+ });
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ }
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ const kTotalAddition = (kNumIterations / 2) * 3;
+ t.expectGPUBufferValuesEqual(
+ buffer1,
+ new Uint32Array([...iterRange(kNumElements, x => x + kTotalAddition)])
+ );
+ t.expectGPUBufferValuesEqual(
+ buffer2,
+ new Uint32Array([...iterRange(kNumElements, x => x + kTotalAddition)])
+ );
+ });
+
+g.test('many_dispatches')
+ .desc(`Tests execution of compute passes with a huge number of dispatch calls`)
+ .fn(async t => {
+ const kNumElements = 64;
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const module = t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = buffer.data[id.x] + 1u;
+ }
+ `,
+ });
+ const kNumIterations = 1_000_000;
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ pass.setBindGroup(0, bindGroup);
+ for (let i = 0; i < kNumIterations; ++i) {
+ pass.dispatchWorkgroups(kNumElements);
+ }
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(
+ buffer,
+ new Uint32Array([...iterRange(kNumElements, x => x + kNumIterations)])
+ );
+ });
+
+g.test('huge_dispatches')
+ .desc(`Tests execution of compute passes with huge dispatch calls`)
+ .fn(async t => {
+ const kDimensions = [512, 512, 128];
+ kDimensions.forEach(x => {
+ assert(x <= t.device.limits.maxComputeWorkgroupsPerDimension);
+ });
+
+ const kNumElements = kDimensions[0] * kDimensions[1] * kDimensions[2];
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const module = t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ let index = (id.z * 512u + id.y) * 512u + id.x;
+ buffer.data[index] = buffer.data[index] + 1u;
+ }
+ `,
+ });
+ const kNumIterations = 16;
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ for (let i = 0; i < kNumIterations; ++i) {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setBindGroup(0, bindGroup);
+ pass.setPipeline(pipeline);
+ pass.dispatchWorkgroups(kDimensions[0], kDimensions[1], kDimensions[2]);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ await t.device.queue.onSubmittedWorkDone();
+ }
+ t.expectGPUBufferValuesEqual(
+ buffer,
+ new Uint32Array([...iterRange(kNumElements, x => x + kNumIterations)])
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/device/README.txt
new file mode 100644
index 0000000000..6ee89fc5fd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/README.txt
@@ -0,0 +1,2 @@
+Stress tests covering GPUDevice usage, primarily focused on stressing allocation
+of various resources.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_allocation.spec.ts
new file mode 100644
index 0000000000..5d428f3edb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_allocation.spec.ts
@@ -0,0 +1,65 @@
+export const description = `
+Stress tests for allocation of GPUBindGroup objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUBindGroup objects.`)
+ .fn(t => {
+ const kNumGroups = 1_000_000;
+ const buffer = t.device.createBuffer({
+ size: 64,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const layout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' },
+ },
+ ],
+ });
+ const bindGroups = [];
+ for (let i = 0; i < kNumGroups; ++i) {
+ bindGroups.push(
+ t.device.createBindGroup({
+ layout,
+ entries: [{ binding: 0, resource: { buffer } }],
+ })
+ );
+ }
+ });
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUBindGroup objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .fn(t => {
+ const kNumGroups = 5_000_000;
+ const buffer = t.device.createBuffer({
+ size: 64,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const layout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' },
+ },
+ ],
+ });
+ for (let i = 0; i < kNumGroups; ++i) {
+ t.device.createBindGroup({
+ layout,
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_layout_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_layout_allocation.spec.ts
new file mode 100644
index 0000000000..0933cd1b59
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/bind_group_layout_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUBindGroupLayout objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUBindGroupLayout objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUBindGroupLayout objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/buffer_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/buffer_allocation.spec.ts
new file mode 100644
index 0000000000..f55ec79c44
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/buffer_allocation.spec.ts
@@ -0,0 +1,25 @@
+export const description = `
+Stress tests for allocation of GPUBuffer objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting').desc(`Tests allocation of many coexisting GPUBuffer objects.`).unimplemented();
+
+g.test('continuous,with_destroy')
+ .desc(
+ `Tests allocation and destruction of many GPUBuffer objects over time. Objects
+are sequentially created and destroyed over a very large number of iterations.`
+ )
+ .unimplemented();
+
+g.test('continuous,no_destroy')
+ .desc(
+ `Tests allocation and implicit GC of many GPUBuffer objects over time. Objects
+are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/command_encoder_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/command_encoder_allocation.spec.ts
new file mode 100644
index 0000000000..e41769ee06
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/command_encoder_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUCommandEncoder objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUCommandEncoder objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUCommandEncoder objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/compute_pipeline_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/compute_pipeline_allocation.spec.ts
new file mode 100644
index 0000000000..5c03bc9674
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/compute_pipeline_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUComputePipeline objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUComputePipeline objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUComputePipeline objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/pipeline_layout_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/pipeline_layout_allocation.spec.ts
new file mode 100644
index 0000000000..15d417fd7e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/pipeline_layout_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUPipelineLayout objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUPipelineLayout objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUPipelineLayout objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/query_set_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/query_set_allocation.spec.ts
new file mode 100644
index 0000000000..757645cbf6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/query_set_allocation.spec.ts
@@ -0,0 +1,27 @@
+export const description = `
+Stress tests for allocation of GPUQuerySet objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUQuerySet objects.`)
+ .unimplemented();
+
+g.test('continuous,with_destroy')
+ .desc(
+ `Tests allocation and destruction of many GPUQuerySet objects over time. Objects
+are sequentially created and destroyed over a very large number of iterations.`
+ )
+ .unimplemented();
+
+g.test('continuous,no_destroy')
+ .desc(
+ `Tests allocation and implicit GC of many GPUQuerySet objects over time. Objects
+are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/render_bundle_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/render_bundle_allocation.spec.ts
new file mode 100644
index 0000000000..d7448412a1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/render_bundle_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPURenderBundle objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPURenderBundle objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPURenderBundle objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/render_pipeline_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/render_pipeline_allocation.spec.ts
new file mode 100644
index 0000000000..21eb92cf7c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/render_pipeline_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPURenderPipeline objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPURenderPipeline objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPURenderPipeline objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/sampler_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/sampler_allocation.spec.ts
new file mode 100644
index 0000000000..c34dae3f67
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/sampler_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUSampler objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUSampler objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUSampler objects over time. Objects
+are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/shader_module_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/shader_module_allocation.spec.ts
new file mode 100644
index 0000000000..97ef73d2c9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/shader_module_allocation.spec.ts
@@ -0,0 +1,20 @@
+export const description = `
+Stress tests for allocation of GPUShaderModule objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUShaderModule objects.`)
+ .unimplemented();
+
+g.test('continuous')
+ .desc(
+ `Tests allocation and implicit GC of many GPUShaderModule objects over time.
+Objects are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/device/texture_allocation.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/device/texture_allocation.spec.ts
new file mode 100644
index 0000000000..5cef598804
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/device/texture_allocation.spec.ts
@@ -0,0 +1,27 @@
+export const description = `
+Stress tests for allocation of GPUTexture objects through GPUDevice.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('coexisting')
+ .desc(`Tests allocation of many coexisting GPUTexture objects.`)
+ .unimplemented();
+
+g.test('continuous,with_destroy')
+ .desc(
+ `Tests allocation and destruction of many GPUTexture objects over time. Objects
+are sequentially created and destroyed over a very large number of iterations.`
+ )
+ .unimplemented();
+
+g.test('continuous,no_destroy')
+ .desc(
+ `Tests allocation and implicit GC of many GPUTexture objects over time. Objects
+are sequentially created and dropped for GC over a very large number of
+iterations.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/listing.ts b/dom/webgpu/tests/cts/checkout/src/stress/listing.ts
new file mode 100644
index 0000000000..823639c692
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/listing.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/no-restricted-paths */
+import { TestSuiteListing } from '../common/internal/test_suite_listing.js';
+import { makeListing } from '../common/tools/crawl.js';
+
+export const listing: Promise<TestSuiteListing> = makeListing(__filename);
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/memory/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/memory/README.txt
new file mode 100644
index 0000000000..ac0c90bfb7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/memory/README.txt
@@ -0,0 +1 @@
+Stress tests covering allocation and usage of various types of GPUBuffer objects.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/memory/churn.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/memory/churn.spec.ts
new file mode 100644
index 0000000000..fcb899eb29
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/memory/churn.spec.ts
@@ -0,0 +1,17 @@
+export const description = `
+Stress tests covering robustness in the presence of heavy buffer and texture
+memory churn.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('churn')
+ .desc(
+ `Allocates and populates a huge number of buffers and textures over time,
+retaining some while dropping or explicitly destroying others. When finished,
+verifies the expected contents of any remaining buffers and textures.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts
new file mode 100644
index 0000000000..3ab542c940
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts
@@ -0,0 +1,45 @@
+export const description = `
+Stress tests covering robustness when available VRAM is exhausted.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+import { exhaustVramUntilUnder64MB } from '../../webgpu/util/memory.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('vram_oom')
+ .desc(`Tests that we can allocate buffers until we run out of VRAM.`)
+ .fn(async t => {
+ await exhaustVramUntilUnder64MB(t.device);
+ });
+
+g.test('get_mapped_range')
+ .desc(
+ `Tests getMappedRange on a mappedAtCreation GPUBuffer that failed allocation due
+to OOM. This should throw a RangeError, but below a certain threshold may just
+crash the page.`
+ )
+ .unimplemented();
+
+g.test('map_after_vram_oom')
+ .desc(
+ `Allocates tons of buffers and textures with varying mapping states (unmappable,
+mappable, mapAtCreation, mapAtCreation-then-unmapped) until OOM; then attempts
+to mapAsync all the mappable objects.`
+ )
+ .unimplemented();
+
+g.test('validation_vs_oom')
+ .desc(
+ `Tests that calls affected by both OOM and validation errors expose the
+validation error with precedence.`
+ )
+ .unimplemented();
+
+g.test('recovery')
+ .desc(
+ `Tests that after going VRAM-OOM, destroying allocated resources eventually
+allows new resources to be allocated.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queries/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/queries/README.txt
new file mode 100644
index 0000000000..fe466205c4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queries/README.txt
@@ -0,0 +1 @@
+Stress tests covering use of GPUQuerySet objects and related operations.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queries/occlusion.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/queries/occlusion.spec.ts
new file mode 100644
index 0000000000..056d6bdaea
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queries/occlusion.spec.ts
@@ -0,0 +1,10 @@
+export const description = `
+Stress tests for occlusion queries.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('many').desc(`Tests a huge number of occlusion queries in a render pass.`).unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queries/pipeline_statistics.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/queries/pipeline_statistics.spec.ts
new file mode 100644
index 0000000000..ce8a16f462
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queries/pipeline_statistics.spec.ts
@@ -0,0 +1,38 @@
+export const description = `
+Stress tests for pipeline statistics queries.
+
+TODO: pipeline statistics queries are removed from core; consider moving tests to another suite.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('render_pass_one_query_set')
+ .desc(
+ `Tests a huge number of pipeline statistics queries over a single query set in a
+single render pass.`
+ )
+ .unimplemented();
+
+g.test('render_pass_many_query_sets')
+ .desc(
+ `Tests a huge number of pipeline statistics queries over a huge number of query
+sets in a single render pass.`
+ )
+ .unimplemented();
+
+g.test('compute_pass_one_query_set')
+ .desc(
+ `Tests a huge number of pipeline statistics queries over a single query set in a
+single compute pass.`
+ )
+ .unimplemented();
+
+g.test('compute_pass_many_query_sets')
+ .desc(
+ `Tests a huge number of pipeline statistics queries over a huge number of query
+sets in a single compute pass.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queries/resolve.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/queries/resolve.spec.ts
new file mode 100644
index 0000000000..da67977395
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queries/resolve.spec.ts
@@ -0,0 +1,15 @@
+export const description = `
+Stress tests for query resolution.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('many_large_sets')
+ .desc(
+ `Tests a huge number of resolveQuerySet operations on a huge number of
+query sets between render passes.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queries/timestamps.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/queries/timestamps.spec.ts
new file mode 100644
index 0000000000..da3e1eb472
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queries/timestamps.spec.ts
@@ -0,0 +1,50 @@
+export const description = `
+Stress tests for timestamp queries.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('command_encoder_one_query_set')
+ .desc(
+ `Tests a huge number of timestamp queries over a single query set between render
+passes on a single command encoder.`
+ )
+ .unimplemented();
+
+g.test('command_encoder_many_query_sets')
+ .desc(
+ `Tests a huge number of timestamp queries over many query sets between render
+passes on a single command encoder.`
+ )
+ .unimplemented();
+
+g.test('render_pass_one_query_set')
+ .desc(
+ `Tests a huge number of timestamp queries over a single query set in a single
+render pass.`
+ )
+ .unimplemented();
+
+g.test('render_pass_many_query_sets')
+ .desc(
+ `Tests a huge number of timestamp queries over a huge number of query sets in a
+single render pass.`
+ )
+ .unimplemented();
+
+g.test('compute_pass_one_query_set')
+ .desc(
+ `Tests a huge number of timestamp queries over a single query set in a single
+compute pass.`
+ )
+ .unimplemented();
+
+g.test('compute_pass_many_query_sets')
+ .desc(
+ `Tests a huge number of timestamp queries over a huge number of query sets in a
+single compute pass.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queue/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/queue/README.txt
new file mode 100644
index 0000000000..adb4ec40ce
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queue/README.txt
@@ -0,0 +1 @@
+Stress tests covering GPUQueue usage.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/queue/submit.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/queue/submit.spec.ts
new file mode 100644
index 0000000000..e1551727e2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/queue/submit.spec.ts
@@ -0,0 +1,102 @@
+export const description = `
+Stress tests for command submission to GPUQueue objects.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { iterRange } from '../../common/util/util.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('huge_command_buffer')
+ .desc(
+ `Tests submission of huge command buffers to a GPUQueue. Huge buffers are
+encoded by chaining together long sequences of compute passes, with expected
+results verified at the end of the test.`
+ )
+ .fn(async t => {
+ const kNumElements = 64;
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = buffer.data[id.x] + 1u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ const encoder = t.device.createCommandEncoder();
+ const kNumIterations = 500_000;
+ for (let i = 0; i < kNumIterations; ++i) {
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ pass.end();
+ }
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(
+ buffer,
+ new Uint32Array([...iterRange(kNumElements, x => x + kNumIterations)])
+ );
+ });
+
+g.test('many_command_buffers')
+ .desc(
+ `Tests submission of a huge number of command buffers to a GPUQueue by a single
+submit() call.`
+ )
+ .fn(async t => {
+ const kNumElements = 64;
+ const data = new Uint32Array([...iterRange(kNumElements, x => x)]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = buffer.data[id.x] + 1u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ const kNumIterations = 500_000;
+ const buffers = [];
+ for (let i = 0; i < kNumIterations; ++i) {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ pass.end();
+ buffers.push(encoder.finish());
+ }
+ t.device.queue.submit(buffers);
+ t.expectGPUBufferValuesEqual(
+ buffer,
+ new Uint32Array([...iterRange(kNumElements, x => x + kNumIterations)])
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/render/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/render/README.txt
new file mode 100644
index 0000000000..7dcc73fbc3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/render/README.txt
@@ -0,0 +1,3 @@
+Stress tests covering operations specific to GPURenderPipeline, GPURenderPass, and GPURenderBundle.
+
+- Issuing draw calls with huge counts.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/render/render_pass.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/render/render_pass.spec.ts
new file mode 100644
index 0000000000..d064e5f95b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/render/render_pass.spec.ts
@@ -0,0 +1,353 @@
+export const description = `
+Stress tests covering GPURenderPassEncoder usage.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { range } from '../../common/util/util.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('many')
+ .desc(
+ `Tests execution of a huge number of render passes using the same GPURenderPipeline. This uses
+a single render pass for every output fragment, with each pass executing a one-vertex draw call.`
+ )
+ .fn(async t => {
+ const kSize = 1024;
+ const module = t.device.createShaderModule({
+ code: `
+ @vertex fn vmain(@builtin(vertex_index) index: u32)
+ -> @builtin(position) vec4<f32> {
+ let position = vec2<f32>(f32(index % ${kSize}u), f32(index / ${kSize}u));
+ let r = vec2<f32>(1.0 / f32(${kSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kSize, kSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ };
+ const encoder = t.device.createCommandEncoder();
+ range(kSize * kSize, i => {
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.draw(1, 1, i);
+ pass.end();
+ });
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kSize, kSize, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
+
+g.test('pipeline_churn')
+ .desc(
+ `Tests execution of a large number of render pipelines, each within its own render pass. Each
+pass does a single draw call, with one pass per output fragment.`
+ )
+ .fn(async t => {
+ const kWidth = 64;
+ const kHeight = 8;
+ const module = t.device.createShaderModule({
+ code: `
+ @vertex fn vmain(@builtin(vertex_index) index: u32)
+ -> @builtin(position) vec4<f32> {
+ let position = vec2<f32>(f32(index % ${kWidth}u), f32(index / ${kWidth}u));
+ let size = vec2<f32>(f32(${kWidth}), f32(${kHeight}));
+ let r = vec2<f32>(1.0) / size;
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kWidth, kHeight],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const depthTarget = t.device.createTexture({
+ size: [kWidth, kHeight],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ format: 'depth24plus-stencil8',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: depthTarget.createView(),
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'discard',
+ },
+ };
+ const encoder = t.device.createCommandEncoder();
+ range(kWidth * kHeight, i => {
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ depthStencil: {
+ format: 'depth24plus-stencil8',
+
+ // Not really used, but it ensures that each pipeline is unique.
+ depthBias: i,
+ },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.draw(1, 1, i);
+ pass.end();
+ });
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kWidth, kHeight, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
+
+g.test('bind_group_churn')
+ .desc(
+ `Tests execution of render passes which switch between a huge number of bind groups. This uses
+a single render pass with a single pipeline, and one draw call per fragment of the output texture.
+Each draw call is made with a unique bind group 0, with binding 0 referencing a unique uniform
+buffer.`
+ )
+ .fn(async t => {
+ const kSize = 128;
+ const module = t.device.createShaderModule({
+ code: `
+ struct Uniforms { index: u32, };
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
+ @vertex fn vmain() -> @builtin(position) vec4<f32> {
+ let index = uniforms.index;
+ let position = vec2<f32>(f32(index % ${kSize}u), f32(index / ${kSize}u));
+ let r = vec2<f32>(1.0 / f32(${kSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const layout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.VERTEX,
+ buffer: { type: 'uniform' },
+ },
+ ],
+ });
+ const pipeline = t.device.createRenderPipeline({
+ layout: t.device.createPipelineLayout({ bindGroupLayouts: [layout] }),
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kSize, kSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ };
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ range(kSize * kSize, i => {
+ const buffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.UNIFORM,
+ mappedAtCreation: true,
+ });
+ new Uint32Array(buffer.getMappedRange())[0] = i;
+ buffer.unmap();
+ pass.setBindGroup(
+ 0,
+ t.device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer } }] })
+ );
+ pass.draw(1, 1);
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kSize, kSize, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
+
+g.test('many_draws')
+ .desc(
+ `Tests execution of render passes with a huge number of draw calls. This uses a single
+render pass with a single pipeline, and one draw call per fragment of the output texture.`
+ )
+ .fn(async t => {
+ const kSize = 4096;
+ const module = t.device.createShaderModule({
+ code: `
+ @vertex fn vmain(@builtin(vertex_index) index: u32)
+ -> @builtin(position) vec4<f32> {
+ let position = vec2<f32>(f32(index % ${kSize}u), f32(index / ${kSize}u));
+ let r = vec2<f32>(1.0 / f32(${kSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kSize, kSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ };
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ range(kSize * kSize, i => pass.draw(1, 1, i));
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kSize, kSize, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
+
+g.test('huge_draws')
+ .desc(
+ `Tests execution of several render passes with huge draw calls. Each pass uses a single draw
+call which draws multiple vertices for each fragment of a large output texture.`
+ )
+ .fn(async t => {
+ const kSize = 32768;
+ const kTextureSize = 4096;
+ const kVertsPerFragment = (kSize * kSize) / (kTextureSize * kTextureSize);
+ const module = t.device.createShaderModule({
+ code: `
+ @vertex fn vmain(@builtin(vertex_index) vert_index: u32)
+ -> @builtin(position) vec4<f32> {
+ let index = vert_index / ${kVertsPerFragment}u;
+ let position = vec2<f32>(f32(index % ${kTextureSize}u), f32(index / ${kTextureSize}u));
+ let r = vec2<f32>(1.0 / f32(${kTextureSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(position, a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.draw(kSize * kSize);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kTextureSize, kTextureSize, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/render/vertex_buffers.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/render/vertex_buffers.spec.ts
new file mode 100644
index 0000000000..bba129feec
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/render/vertex_buffers.spec.ts
@@ -0,0 +1,130 @@
+export const description = `
+Stress tests covering vertex buffer usage.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+function createHugeVertexBuffer(t: GPUTest, size: number) {
+ const kBufferSize = size * size * 8;
+ const buffer = t.device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<vec2<u32>>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ let base = id.x * ${size}u;
+ for (var x: u32 = 0u; x < ${size}u; x = x + 1u) {
+ buffer.data[base + x] = vec2<u32>(x, id.x);
+ }
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer },
+ },
+ ],
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(size);
+ pass.end();
+
+ const vertexBuffer = t.device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
+ });
+ encoder.copyBufferToBuffer(buffer, 0, vertexBuffer, 0, kBufferSize);
+ t.device.queue.submit([encoder.finish()]);
+ return vertexBuffer;
+}
+
+g.test('many')
+ .desc(`Tests execution of draw calls using a huge vertex buffer.`)
+ .fn(async t => {
+ const kSize = 4096;
+ const buffer = createHugeVertexBuffer(t, kSize);
+ const module = t.device.createShaderModule({
+ code: `
+ @vertex fn vmain(@location(0) position: vec2<u32>)
+ -> @builtin(position) vec4<f32> {
+ let r = vec2<f32>(1.0 / f32(${kSize}));
+ let a = 2.0 * r;
+ let b = r - vec2<f32>(1.0);
+ return vec4<f32>(fma(vec2<f32>(position), a, b), 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 1.0, 1.0);
+ }
+ `,
+ });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vmain',
+ buffers: [
+ {
+ arrayStride: 8,
+ attributes: [
+ {
+ format: 'uint32x2',
+ offset: 0,
+ shaderLocation: 0,
+ },
+ ],
+ },
+ ],
+ },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const renderTarget = t.device.createTexture({
+ size: [kSize, kSize],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.setVertexBuffer(0, buffer);
+ pass.draw(kSize * kSize);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSingleColor(renderTarget, 'rgba8unorm', {
+ size: [kSize, kSize, 1],
+ exp: { R: 1, G: 0, B: 1, A: 1 },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/shaders/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/shaders/README.txt
new file mode 100644
index 0000000000..628b4e86fa
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/shaders/README.txt
@@ -0,0 +1 @@
+Stress tests covering very long-running and/or resource-intensive shaders.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/shaders/entry_points.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/shaders/entry_points.spec.ts
new file mode 100644
index 0000000000..95b647ba73
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/shaders/entry_points.spec.ts
@@ -0,0 +1,78 @@
+export const description = `
+Stress tests covering behavior around shader entry points.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { range } from '../../common/util/util.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const makeCode = (numEntryPoints: number) => {
+ const kBaseCode = `
+ struct Buffer { data: u32, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ fn main() { buffer.data = buffer.data + 1u; }
+ `;
+ const makeEntryPoint = (i: number) => `
+ @compute @workgroup_size(1) fn computeMain${i}() { main(); }
+ `;
+ return kBaseCode + range(numEntryPoints, makeEntryPoint).join('');
+};
+
+g.test('many')
+ .desc(
+ `Tests compilation and usage of shaders with a huge number of entry points.
+
+TODO: There may be a normative limit to the number of entry points allowed in
+a shader, in which case this would become a validation test instead.`
+ )
+ .fn(async t => {
+ const data = new Uint32Array([0]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+
+ // NOTE: Initial shader compilation time seems to scale exponentially with
+ // this value in Chrome.
+ const kNumEntryPoints = 200;
+
+ const shader = t.device.createShaderModule({
+ code: makeCode(kNumEntryPoints),
+ });
+
+ const layout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' },
+ },
+ ],
+ });
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [layout],
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout,
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ range(kNumEntryPoints, i => {
+ const pipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: shader,
+ entryPoint: `computeMain${i}`,
+ },
+ });
+
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ });
+
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(buffer, new Uint32Array([kNumEntryPoints]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/shaders/non_halting.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/shaders/non_halting.spec.ts
new file mode 100644
index 0000000000..b88aa083b3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/shaders/non_halting.spec.ts
@@ -0,0 +1,194 @@
+export const description = `
+Stress tests covering robustness in the presence of non-halting shaders.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('compute')
+ .desc(
+ `Tests execution of compute passes with non-halting dispatch operations.
+
+This is expected to hang for a bit, but it should ultimately result in graceful
+device loss.`
+ )
+ .fn(async t => {
+ const data = new Uint32Array([0]);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const module = t.device.createShaderModule({
+ code: `
+ struct Buffer { data: u32, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main() {
+ loop {
+ if (buffer.data == 1u) {
+ break;
+ }
+ buffer.data = buffer.data + 2u;
+ }
+ }
+ `,
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ await t.device.lost;
+ });
+
+g.test('vertex')
+ .desc(
+ `Tests execution of render passes with a non-halting vertex stage.
+
+This is expected to hang for a bit, but it should ultimately result in graceful
+device loss.`
+ )
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ struct Data { counter: u32, increment: u32, };
+ @group(0) @binding(0) var<uniform> data: Data;
+ @vertex fn vmain() -> @builtin(position) vec4<f32> {
+ var counter: u32 = data.counter;
+ loop {
+ if (counter % 2u == 1u) {
+ break;
+ }
+ counter = counter + data.increment;
+ }
+ return vec4<f32>(1.0, 1.0, 0.0, f32(counter));
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0);
+ }
+ `,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const uniforms = t.makeBufferWithContents(new Uint32Array([0, 2]), GPUBufferUsage.UNIFORM);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: uniforms },
+ },
+ ],
+ });
+ const renderTarget = t.device.createTexture({
+ size: [1, 1],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ await t.device.lost;
+ });
+
+g.test('fragment')
+ .desc(
+ `Tests execution of render passes with a non-halting fragment stage.
+
+This is expected to hang for a bit, but it should ultimately result in graceful
+device loss.`
+ )
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ struct Data { counter: u32, increment: u32, };
+ @group(0) @binding(0) var<uniform> data: Data;
+ @vertex fn vmain() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ var counter: u32 = data.counter;
+ loop {
+ if (counter % 2u == 1u) {
+ break;
+ }
+ counter = counter + data.increment;
+ }
+ return vec4<f32>(1.0 / f32(counter), 0.0, 0.0, 1.0);
+ }
+ `,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const uniforms = t.makeBufferWithContents(new Uint32Array([0, 2]), GPUBufferUsage.UNIFORM);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: uniforms },
+ },
+ ],
+ });
+ const renderTarget = t.device.createTexture({
+ size: [1, 1],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ await t.device.lost;
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/shaders/slow.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/shaders/slow.spec.ts
new file mode 100644
index 0000000000..9359b976a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/shaders/slow.spec.ts
@@ -0,0 +1,195 @@
+export const description = `
+Stress tests covering robustness in the presence of slow shaders.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('compute')
+ .desc(`Tests execution of compute passes with very long-running dispatch operations.`)
+ .fn(async t => {
+ const kDispatchSize = 1000;
+ const data = new Uint32Array(kDispatchSize);
+ const buffer = t.makeBufferWithContents(data, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
+ const module = t.device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ loop {
+ if (buffer.data[id.x] == 1000000u) {
+ break;
+ }
+ buffer.data[id.x] = buffer.data[id.x] + 1u;
+ }
+ }
+ `,
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kDispatchSize);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(buffer, new Uint32Array(new Array(kDispatchSize).fill(1000000)));
+ });
+
+g.test('vertex')
+ .desc(`Tests execution of render passes with a very long-running vertex stage.`)
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ struct Data { counter: u32, increment: u32, };
+ @group(0) @binding(0) var<uniform> data: Data;
+ @vertex fn vmain() -> @builtin(position) vec4<f32> {
+ var counter: u32 = data.counter;
+ loop {
+ counter = counter + data.increment;
+ if (counter % 50000000u == 0u) {
+ break;
+ }
+ }
+ return vec4<f32>(1.0, 1.0, 0.0, f32(counter));
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 0.0, 1.0);
+ }
+ `,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const uniforms = t.makeBufferWithContents(new Uint32Array([0, 1]), GPUBufferUsage.UNIFORM);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: uniforms },
+ },
+ ],
+ });
+ const renderTarget = t.device.createTexture({
+ size: [3, 3],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSinglePixelIn2DTexture(
+ renderTarget,
+ 'rgba8unorm',
+ { x: 1, y: 1 },
+ {
+ exp: new Uint8Array([255, 255, 0, 255]),
+ }
+ );
+ });
+
+g.test('fragment')
+ .desc(`Tests execution of render passes with a very long-running fragment stage.`)
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ struct Data { counter: u32, increment: u32, };
+ @group(0) @binding(0) var<uniform> data: Data;
+ @vertex fn vmain() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+ @fragment fn fmain() -> @location(0) vec4<f32> {
+ var counter: u32 = data.counter;
+ loop {
+ counter = counter + data.increment;
+ if (counter % 50000000u == 0u) {
+ break;
+ }
+ }
+ return vec4<f32>(1.0, 1.0, 1.0 / f32(counter), 1.0);
+ }
+ `,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain', buffers: [] },
+ primitive: { topology: 'point-list' },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module,
+ entryPoint: 'fmain',
+ },
+ });
+ const uniforms = t.makeBufferWithContents(new Uint32Array([0, 1]), GPUBufferUsage.UNIFORM);
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: uniforms },
+ },
+ ],
+ });
+ const renderTarget = t.device.createTexture({
+ size: [3, 3],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.expectSinglePixelIn2DTexture(
+ renderTarget,
+ 'rgba8unorm',
+ { x: 1, y: 1 },
+ {
+ exp: new Uint8Array([255, 255, 0, 255]),
+ }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/texture/README.txt b/dom/webgpu/tests/cts/checkout/src/stress/texture/README.txt
new file mode 100644
index 0000000000..db40963b2e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/texture/README.txt
@@ -0,0 +1 @@
+Stress tests covering texture usage.
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/texture/large.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/texture/large.spec.ts
new file mode 100644
index 0000000000..cba2053d38
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/texture/large.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Stress tests covering usage of very large textures.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('loading,2d')
+ .desc(
+ `Tests execution of shaders loading values from very large (up to at least
+8192x8192) 2D textures. The texture size is selected according to the limit
+supported by the GPUDevice.`
+ )
+ .unimplemented();
+
+g.test('loading,2d_array')
+ .desc(
+ `Tests execution of shaders loading values from very large (up to at least
+8192x8192x2048) arrays of 2D textures. The texture and array size is selected
+according to limits supported by the GPUDevice.`
+ )
+ .unimplemented();
+
+g.test('loading,3d')
+ .desc(
+ `Tests execution of shaders loading values from very large (up to at least
+2048x2048x2048) textures. The texture size is selected according to the limit
+supported by the GPUDevice.`
+ )
+ .unimplemented();
+
+g.test('sampling,2d')
+ .desc(
+ `Tests execution of shaders sampling values from very large (up to at least
+8192x8192) 2D textures. The texture size is selected according to the limit
+supported by the GPUDevice.`
+ )
+ .unimplemented();
+
+g.test('sampling,2d_array')
+ .desc(
+ `Tests execution of shaders sampling values from very large (up to at least
+8192x8192x2048) arrays of 2D textures. The texture and array size is selected
+according to limits supported by the GPUDevice.`
+ )
+ .unimplemented();
+
+g.test('sampling,3d')
+ .desc(
+ `Tests execution of shaders sampling values from very large (up to at least
+2048x2048x2048) textures. The texture size is selected according to the limit
+supported by the GPUDevice.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/README.txt b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt
new file mode 100644
index 0000000000..17272c3919
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt
@@ -0,0 +1 @@
+Unit tests for CTS framework.
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts
new file mode 100644
index 0000000000..0d7b0b2f56
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts
@@ -0,0 +1,167 @@
+export const description = `
+Tests for eventualAsyncExpectation and immediateAsyncExpectation.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { assert, objectEquals, rejectOnTimeout, resolveOnTimeout } from '../common/util/util.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+class FixtureToTest extends UnitTest {
+ public immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> {
+ return super.immediateAsyncExpectation(fn);
+ }
+ public eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void {
+ super.eventualAsyncExpectation(fn);
+ }
+}
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('eventual').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(FixtureToTest);
+
+ const runState = [0, 0, 0, 0];
+ let runStateIndex = 0;
+
+ // Should pass in state 3
+ g.test('noawait,resolve').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50);
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 4
+ g.test('noawait,reject').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await rejectOnTimeout(50, 'rejected 1');
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 3
+ g.test('nested,2').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation
+ t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 1'));
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 3
+ g.test('nested,4').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation
+ t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 2'));
+ runState[idx] = 3;
+ });
+ });
+ });
+ runState[idx] = 4;
+ });
+
+ const resultsPromise = t0.run(g);
+ assert(objectEquals(runState, [0, 0, 0, 0]));
+
+ const statuses = Array.from(await resultsPromise).map(([, v]) => v.status);
+ assert(objectEquals(runState, [3, 4, 3, 3]), () => runState.toString());
+ assert(objectEquals(statuses, ['pass', 'fail', 'fail', 'fail']), () => statuses.toString());
+});
+
+g.test('immediate').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(FixtureToTest);
+
+ const runState = [0, 0, 0, 0, 0];
+
+ g.test('noawait,resolve').fn(t => {
+ runState[0] = 1;
+ void t.immediateAsyncExpectation(async () => {
+ runState[0] = 2;
+ await resolveOnTimeout(50);
+ runState[0] = 3;
+ });
+ runState[0] = 4;
+ });
+
+ // (Can't g.test('noawait,reject') because it causes a top-level Promise
+ // rejection which crashes Node.)
+
+ g.test('await,resolve').fn(async t => {
+ runState[1] = 1;
+ await t.immediateAsyncExpectation(async () => {
+ runState[1] = 2;
+ await resolveOnTimeout(50);
+ runState[1] = 3;
+ });
+ });
+
+ g.test('await,reject').fn(async t => {
+ runState[2] = 1;
+ await t.immediateAsyncExpectation(async () => {
+ runState[2] = 2;
+ await rejectOnTimeout(50, 'rejected 3');
+ runState[2] = 3;
+ });
+ });
+
+ // (Similarly can't test 'nested,noawait'.)
+
+ g.test('nested,await,2').fn(t => {
+ runState[3] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[3] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation
+ runState[3] = 3;
+ await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3'));
+ runState[3] = 5;
+ });
+ runState[3] = 4;
+ });
+
+ g.test('nested,await,4').fn(t => {
+ runState[4] = 1;
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ runState[4] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation
+ runState[4] = 3;
+ await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3'));
+ runState[4] = 5;
+ });
+ });
+ });
+ runState[4] = 4;
+ });
+
+ const resultsPromise = t0.run(g);
+ assert(objectEquals(runState, [0, 0, 0, 0, 0]));
+
+ const statuses = Array.from(await resultsPromise).map(([, v]) => v.status);
+ assert(objectEquals(runState, [3, 3, 2, 3, 3]));
+ assert(objectEquals(statuses, ['fail', 'pass', 'fail', 'fail', 'fail']));
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts
new file mode 100644
index 0000000000..f80444f03b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts
@@ -0,0 +1,35 @@
+export const description = `
+Basic unit tests for test framework.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('test,sync').fn(t => {});
+
+g.test('test,async').fn(async t => {});
+
+g.test('test_with_params,sync')
+ .paramsSimple([{}])
+ .fn(t => {
+ t.debug(JSON.stringify(t.params));
+ });
+
+g.test('test_with_params,async')
+ .paramsSimple([{}])
+ .fn(async t => {
+ t.debug(JSON.stringify(t.params));
+ });
+
+g.test('test_with_params,private_params')
+ .paramsSimple([
+ { a: 1, b: 2, _result: 3 }, //
+ { a: 4, b: -3, _result: 1 },
+ ])
+ .fn(t => {
+ const { a, b, _result } = t.params;
+ t.expect(a + b === _result);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts
new file mode 100644
index 0000000000..1a722a1b86
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts
@@ -0,0 +1,71 @@
+export const description = `Unit tests for check_contents`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { ErrorWithExtra } from '../common/util/util.js';
+import { checkElementsEqual } from '../webgpu/util/check_contents.js';
+
+class F extends Fixture {
+ test(substr: undefined | string, result: undefined | ErrorWithExtra) {
+ if (substr === undefined) {
+ this.expect(result === undefined, result?.message);
+ } else {
+ this.expect(result !== undefined && result.message.indexOf(substr) !== -1, result?.message);
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('checkElementsEqual').fn(t => {
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array(), new Uint16Array()));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint32Array(), new Float32Array()));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([]), new Uint8Array([0])));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([0]), new Uint8Array([])));
+ {
+ t.test(undefined, checkElementsEqual(new Uint8Array([]), new Uint8Array([])));
+ t.test(undefined, checkElementsEqual(new Uint8Array([0]), new Uint8Array([0])));
+ t.test(undefined, checkElementsEqual(new Uint8Array([1]), new Uint8Array([1])));
+ t.test(
+ `
+ Starting at index 0:
+ actual == 0x: 00
+ failed -> xx
+ expected == 01`,
+ checkElementsEqual(new Uint8Array([0]), new Uint8Array([1]))
+ );
+ t.test(
+ 'expected == 01 02 01',
+ checkElementsEqual(new Uint8Array([1, 1, 1]), new Uint8Array([1, 2, 1]))
+ );
+ }
+ {
+ const actual = new Uint8Array(280);
+ const exp = new Uint8Array(280);
+ for (let i = 2; i < 20; ++i) actual[i] = i - 4;
+ t.test(
+ '00 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00',
+ checkElementsEqual(actual, exp)
+ );
+ for (let i = 2; i < 280; ++i) actual[i] = i - 4;
+ t.test('Starting at index 1:', checkElementsEqual(actual, exp));
+ for (let i = 0; i < 2; ++i) actual[i] = i - 4;
+ t.test('Starting at index 0:', checkElementsEqual(actual, exp));
+ }
+ {
+ const actual = new Int32Array(30);
+ const exp = new Int32Array(30);
+ for (let i = 2; i < 7; ++i) actual[i] = i - 3;
+ t.test('00000002 00000003 00000000\n', checkElementsEqual(actual, exp));
+ for (let i = 2; i < 30; ++i) actual[i] = i - 3;
+ t.test('00000000 00000000 ...', checkElementsEqual(actual, exp));
+ }
+ {
+ const actual = new Float64Array(30);
+ const exp = new Float64Array(30);
+ for (let i = 2; i < 7; ++i) actual[i] = (i - 4) * 1e100;
+ t.test('2.000e+100 0.000\n', checkElementsEqual(actual, exp));
+ for (let i = 2; i < 280; ++i) actual[i] = (i - 4) * 1e100;
+ t.test('6.000e+100 7.000e+100 ...', checkElementsEqual(actual, exp));
+ }
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts
new file mode 100644
index 0000000000..1255b4c548
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts
@@ -0,0 +1,408 @@
+export const description = `Unit tests for conversion`;
+
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import { kValue } from '../webgpu/util/constants.js';
+import {
+ bool,
+ f16Bits,
+ f32,
+ f32Bits,
+ float16BitsToFloat32,
+ float32ToFloat16Bits,
+ float32ToFloatBits,
+ floatBitsToNormalULPFromZero,
+ floatBitsToNumber,
+ i32,
+ kFloat16Format,
+ kFloat32Format,
+ pack2x16float,
+ pack2x16snorm,
+ pack2x16unorm,
+ pack4x8snorm,
+ pack4x8unorm,
+ Scalar,
+ u32,
+ vec2,
+ vec3,
+ vec4,
+ Vector,
+} from '../webgpu/util/conversion.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+const cases = [
+ [0b0_01111_0000000000, 1],
+ [0b0_00001_0000000000, 0.00006103515625],
+ [0b0_01101_0101010101, 0.33325195],
+ [0b0_11110_1111111111, 65504],
+ [0b0_00000_0000000000, 0],
+ [0b0_01110_0000000000, 0.5],
+ [0b0_01100_1001100110, 0.1999512],
+ [0b0_01111_0000000001, 1.00097656],
+ [0b0_10101_1001000000, 100],
+ [0b1_01100_1001100110, -0.1999512],
+ [0b1_10101_1001000000, -100],
+];
+
+g.test('float16BitsToFloat32').fn(t => {
+ for (const [bits, number] of [
+ ...cases,
+ [0b1_00000_0000000000, -0], // (resulting sign is not actually tested)
+ [0b0_00000_1111111111, 0.00006104], // subnormal f16 input
+ [0b1_00000_1111111111, -0.00006104],
+ ]) {
+ const actual = float16BitsToFloat32(bits);
+ t.expect(
+ // some loose check
+ Math.abs(actual - number) <= 0.00001,
+ `for ${bits.toString(2)}, expected ${number}, got ${actual}`
+ );
+ }
+});
+
+g.test('float32ToFloat16Bits').fn(t => {
+ for (const [bits, number] of [
+ ...cases,
+ [0b0_00000_0000000000, 0.00001], // input that becomes subnormal in f16 is rounded to 0
+ [0b1_00000_0000000000, -0.00001], // and sign is preserved
+ ]) {
+ // some loose check
+ const actual = float32ToFloat16Bits(number);
+ t.expect(
+ Math.abs(actual - bits) <= 1,
+ `for ${number}, expected ${bits.toString(2)}, got ${actual.toString(2)}`
+ );
+ }
+});
+
+g.test('float32ToFloatBits_floatBitsToNumber')
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('signed', [0, 1] as const)
+ .combine('exponentBits', [5, 8])
+ .combine('mantissaBits', [10, 23])
+ )
+ .fn(t => {
+ const { signed, exponentBits, mantissaBits } = t.params;
+ const bias = (1 << (exponentBits - 1)) - 1;
+
+ for (const [, value] of cases) {
+ if (value < 0 && signed === 0) continue;
+ const bits = float32ToFloatBits(value, signed, exponentBits, mantissaBits, bias);
+ const reconstituted = floatBitsToNumber(bits, { signed, exponentBits, mantissaBits, bias });
+ t.expect(Math.abs(reconstituted - value) <= 0.0000001, `${reconstituted} vs ${value}`);
+ }
+ });
+
+g.test('floatBitsToULPFromZero,16').fn(t => {
+ const test = (bits: number, ulpFromZero: number) =>
+ t.expect(floatBitsToNormalULPFromZero(bits, kFloat16Format) === ulpFromZero, bits.toString(2));
+ // Zero
+ test(0b0_00000_0000000000, 0);
+ // Subnormal
+ test(0b0_00000_0000000001, 0);
+ test(0b1_00000_0000000001, 0);
+ test(0b0_00000_1111111111, 0);
+ test(0b1_00000_1111111111, 0);
+ // Normal
+ test(0b0_00001_0000000000, 1); // 0 + 1ULP
+ test(0b1_00001_0000000000, -1); // 0 - 1ULP
+ test(0b0_00001_0000000001, 2); // 0 + 2ULP
+ test(0b1_00001_0000000001, -2); // 0 - 2ULP
+ test(0b0_01110_0000000000, 0b01101_0000000001); // 0.5
+ test(0b1_01110_0000000000, -0b01101_0000000001); // -0.5
+ test(0b0_01110_1111111110, 0b01101_1111111111); // 1.0 - 2ULP
+ test(0b1_01110_1111111110, -0b01101_1111111111); // -(1.0 - 2ULP)
+ test(0b0_01110_1111111111, 0b01110_0000000000); // 1.0 - 1ULP
+ test(0b1_01110_1111111111, -0b01110_0000000000); // -(1.0 - 1ULP)
+ test(0b0_01111_0000000000, 0b01110_0000000001); // 1.0
+ test(0b1_01111_0000000000, -0b01110_0000000001); // -1.0
+ test(0b0_01111_0000000001, 0b01110_0000000010); // 1.0 + 1ULP
+ test(0b1_01111_0000000001, -0b01110_0000000010); // -(1.0 + 1ULP)
+ test(0b0_10000_0000000000, 0b01111_0000000001); // 2.0
+ test(0b1_10000_0000000000, -0b01111_0000000001); // -2.0
+
+ const testThrows = (b: number) =>
+ t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat16Format));
+ // Infinity
+ testThrows(0b0_11111_0000000000);
+ testThrows(0b1_11111_0000000000);
+ // NaN
+ testThrows(0b0_11111_1111111111);
+ testThrows(0b1_11111_1111111111);
+});
+
+g.test('floatBitsToULPFromZero,32').fn(t => {
+ const test = (bits: number, ulpFromZero: number) =>
+ t.expect(floatBitsToNormalULPFromZero(bits, kFloat32Format) === ulpFromZero, bits.toString(2));
+ // Zero
+ test(0b0_00000000_00000000000000000000000, 0);
+ // Subnormal
+ test(0b0_00000000_00000000000000000000001, 0);
+ test(0b1_00000000_00000000000000000000001, 0);
+ test(0b0_00000000_11111111111111111111111, 0);
+ test(0b1_00000000_11111111111111111111111, 0);
+ // Normal
+ test(0b0_00000001_00000000000000000000000, 1); // 0 + 1ULP
+ test(0b1_00000001_00000000000000000000000, -1); // 0 - 1ULP
+ test(0b0_00000001_00000000000000000000001, 2); // 0 + 2ULP
+ test(0b1_00000001_00000000000000000000001, -2); // 0 - 2ULP
+ test(0b0_01111110_00000000000000000000000, 0b01111101_00000000000000000000001); // 0.5
+ test(0b1_01111110_00000000000000000000000, -0b01111101_00000000000000000000001); // -0.5
+ test(0b0_01111110_11111111111111111111110, 0b01111101_11111111111111111111111); // 1.0 - 2ULP
+ test(0b1_01111110_11111111111111111111110, -0b01111101_11111111111111111111111); // -(1.0 - 2ULP)
+ test(0b0_01111110_11111111111111111111111, 0b01111110_00000000000000000000000); // 1.0 - 1ULP
+ test(0b1_01111110_11111111111111111111111, -0b01111110_00000000000000000000000); // -(1.0 - 1ULP)
+ test(0b0_01111111_00000000000000000000000, 0b01111110_00000000000000000000001); // 1.0
+ test(0b1_01111111_00000000000000000000000, -0b01111110_00000000000000000000001); // -1.0
+ test(0b0_01111111_00000000000000000000001, 0b01111110_00000000000000000000010); // 1.0 + 1ULP
+ test(0b1_01111111_00000000000000000000001, -0b01111110_00000000000000000000010); // -(1.0 + 1ULP)
+ test(0b0_11110000_00000000000000000000000, 0b11101111_00000000000000000000001); // 2.0
+ test(0b1_11110000_00000000000000000000000, -0b11101111_00000000000000000000001); // -2.0
+
+ const testThrows = (b: number) =>
+ t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat32Format));
+ // Infinity
+ testThrows(0b0_11111111_00000000000000000000000);
+ testThrows(0b1_11111111_00000000000000000000000);
+ // NaN
+ testThrows(0b0_11111111_11111111111111111111111);
+ testThrows(0b0_11111111_00000000000000000000001);
+ testThrows(0b1_11111111_11111111111111111111111);
+ testThrows(0b1_11111111_00000000000000000000001);
+});
+
+g.test('scalarWGSL').fn(t => {
+ const cases: Array<[Scalar, string]> = [
+ [f32(0.0), '0.0f'],
+ [f32(1.0), '1.0f'],
+ [f32(-1.0), '-1.0f'],
+ [f32Bits(0x70000000), '1.5845632502852868e+29f'],
+ [f32Bits(0xf0000000), '-1.5845632502852868e+29f'],
+ [f16Bits(0), '0.0h'],
+ [f16Bits(0x3c00), '1.0h'],
+ [f16Bits(0xbc00), '-1.0h'],
+ [u32(0), '0u'],
+ [u32(1), '1u'],
+ [u32(2000000000), '2000000000u'],
+ [u32(-1), '4294967295u'],
+ [i32(0), 'i32(0)'],
+ [i32(1), 'i32(1)'],
+ [i32(-1), 'i32(-1)'],
+ [bool(true), 'true'],
+ [bool(false), 'false'],
+ ];
+ for (const [value, expect] of cases) {
+ const got = value.wgsl();
+ t.expect(
+ got === expect,
+ `[value: ${value.value}, type: ${value.type}]
+got: ${got}
+expect: ${expect}`
+ );
+ }
+});
+
+g.test('vectorWGSL').fn(t => {
+ const cases: Array<[Vector, string]> = [
+ [vec2(f32(42.0), f32(24.0)), 'vec2(42.0f, 24.0f)'],
+ [vec2(f16Bits(0x5140), f16Bits(0x4e00)), 'vec2(42.0h, 24.0h)'],
+ [vec2(u32(42), u32(24)), 'vec2(42u, 24u)'],
+ [vec2(i32(42), i32(24)), 'vec2(i32(42), i32(24))'],
+ [vec2(bool(false), bool(true)), 'vec2(false, true)'],
+
+ [vec3(f32(0.0), f32(1.0), f32(-1.0)), 'vec3(0.0f, 1.0f, -1.0f)'],
+ [vec3(f16Bits(0), f16Bits(0x3c00), f16Bits(0xbc00)), 'vec3(0.0h, 1.0h, -1.0h)'],
+ [vec3(u32(0), u32(1), u32(-1)), 'vec3(0u, 1u, 4294967295u)'],
+ [vec3(i32(0), i32(1), i32(-1)), 'vec3(i32(0), i32(1), i32(-1))'],
+ [vec3(bool(true), bool(false), bool(true)), 'vec3(true, false, true)'],
+
+ [vec4(f32(1.0), f32(-2.0), f32(4.0), f32(-8.0)), 'vec4(1.0f, -2.0f, 4.0f, -8.0f)'],
+ [
+ vec4(f16Bits(0xbc00), f16Bits(0x4000), f16Bits(0xc400), f16Bits(0x4800)),
+ 'vec4(-1.0h, 2.0h, -4.0h, 8.0h)',
+ ],
+ [vec4(u32(1), u32(-2), u32(4), u32(-8)), 'vec4(1u, 4294967294u, 4u, 4294967288u)'],
+ [vec4(i32(1), i32(-2), i32(4), i32(-8)), 'vec4(i32(1), i32(-2), i32(4), i32(-8))'],
+ [vec4(bool(false), bool(true), bool(true), bool(false)), 'vec4(false, true, true, false)'],
+ ];
+ for (const [value, expect] of cases) {
+ const got = value.wgsl();
+ t.expect(
+ got === expect,
+ `[values: ${value.elements}, type: ${value.type}]
+got: ${got}
+expect: ${expect}`
+ );
+ }
+});
+
+g.test('pack2x16float')
+ .paramsSimple([
+ // f16 normals
+ { inputs: [0, 0], result: [0x00000000, 0x80000000, 0x00008000, 0x80008000] },
+ { inputs: [1, 0], result: [0x00003c00, 0x80003c00] },
+ { inputs: [1, 1], result: [0x3c003c00] },
+ { inputs: [-1, -1], result: [0xbc00bc00] },
+ { inputs: [10, 1], result: [0x3c004900] },
+ { inputs: [-10, 1], result: [0x3c00c900] },
+
+ // f32 normal, but not f16 precise
+ { inputs: [1.00000011920928955078125, 1], result: [0x3c003c00, 0x3c003c01] },
+
+ // f32 subnormals
+ // prettier-ignore
+ { inputs: [kValue.f32.subnormal.positive.max, 1], result: [0x3c000000, 0x3c008000, 0x3c000001] },
+ // prettier-ignore
+ { inputs: [kValue.f32.subnormal.negative.min, 1], result: [0x3c008001, 0x3c000000, 0x3c008000] },
+
+ // f16 subnormals
+ // prettier-ignore
+ { inputs: [kValue.f16.subnormal.positive.max, 1], result: [0x3c0003ff, 0x3c000000, 0x3c008000] },
+ // prettier-ignore
+ { inputs: [kValue.f16.subnormal.negative.min, 1], result: [0x03c0083ff, 0x3c000000, 0x3c008000] },
+
+ // f16 out of bounds
+ { inputs: [kValue.f16.positive.max + 1, 1], result: [undefined] },
+ { inputs: [kValue.f16.negative.min - 1, 1], result: [undefined] },
+ { inputs: [1, kValue.f16.positive.max + 1], result: [undefined] },
+ { inputs: [1, kValue.f16.negative.min - 1], result: [undefined] },
+ ] as const)
+ .fn(test => {
+ const toString = (data: readonly (undefined | number)[]): String[] => {
+ return data.map(d => (d !== undefined ? u32(d).toString() : 'undefined'));
+ };
+
+ const inputs = test.params.inputs;
+ const got = pack2x16float(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ const got_str = toString(got);
+ const expect_str = toString(expect);
+
+ // Using strings of the outputs, so they can be easily sorted, since order of the results doesn't matter.
+ test.expect(
+ objectEquals(got_str.sort(), expect_str.sort()),
+ `pack2x16float(${inputs}) returned [${got_str}]. Expected [${expect_str}]`
+ );
+ });
+
+g.test('pack2x16snorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0], result: 0x00000000 },
+ { inputs: [1, 0], result: 0x00007fff },
+ { inputs: [0, 1], result: 0x7fff0000 },
+ { inputs: [1, 1], result: 0x7fff7fff },
+ { inputs: [-1, -1], result: 0x80018001 },
+ { inputs: [10, 10], result: 0x7fff7fff },
+ { inputs: [-10, -10], result: 0x80018001 },
+ { inputs: [0.1, 0.1], result: 0x0ccd0ccd },
+ { inputs: [-0.1, -0.1], result: 0xf333f333 },
+ { inputs: [0.5, 0.5], result: 0x40004000 },
+ { inputs: [-0.5, -0.5], result: 0xc001c001 },
+ { inputs: [0.1, 0.5], result: 0x40000ccd },
+ { inputs: [-0.1, -0.5], result: 0xc001f333 },
+
+ // Subnormals
+ { inputs: [kValue.f32.subnormal.positive.max, 1], result: 0x7fff0000 },
+ { inputs: [kValue.f32.subnormal.negative.min, 1], result: 0x7fff0000 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack2x16snorm(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack2x16snorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
+
+g.test('pack2x16unorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0], result: 0x00000000 },
+ { inputs: [1, 0], result: 0x0000ffff },
+ { inputs: [0, 1], result: 0xffff0000 },
+ { inputs: [1, 1], result: 0xffffffff },
+ { inputs: [-1, -1], result: 0x00000000 },
+ { inputs: [0.1, 0.1], result: 0x199a199a },
+ { inputs: [0.5, 0.5], result: 0x80008000 },
+ { inputs: [0.1, 0.5], result: 0x8000199a },
+ { inputs: [10, 10], result: 0xffffffff },
+
+ // Subnormals
+ { inputs: [kValue.f32.subnormal.positive.max, 1], result: 0xffff0000 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack2x16unorm(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack2x16unorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
+
+g.test('pack4x8snorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0, 0, 0], result: 0x00000000 },
+ { inputs: [1, 0, 0, 0], result: 0x0000007f },
+ { inputs: [0, 1, 0, 0], result: 0x00007f00 },
+ { inputs: [0, 0, 1, 0], result: 0x007f0000 },
+ { inputs: [0, 0, 0, 1], result: 0x7f000000 },
+ { inputs: [1, 1, 1, 1], result: 0x7f7f7f7f },
+ { inputs: [10, 10, 10, 10], result: 0x7f7f7f7f },
+ { inputs: [-1, 0, 0, 0], result: 0x00000081 },
+ { inputs: [0, -1, 0, 0], result: 0x00008100 },
+ { inputs: [0, 0, -1, 0], result: 0x00810000 },
+ { inputs: [0, 0, 0, -1], result: 0x81000000 },
+ { inputs: [-1, -1, -1, -1], result: 0x81818181 },
+ { inputs: [-10, -10, -10, -10], result: 0x81818181 },
+ { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x0d0d0d0d },
+ { inputs: [-0.1, -0.1, -0.1, -0.1], result: 0xf3f3f3f3 },
+ { inputs: [0.1, -0.1, 0.1, -0.1], result: 0xf30df30d },
+ { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x40404040 },
+ { inputs: [-0.5, -0.5, -0.5, -0.5], result: 0xc1c1c1c1 },
+ { inputs: [-0.5, 0.5, -0.5, 0.5], result: 0x40c140c1 },
+ { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x400d400d },
+ { inputs: [-0.1, -0.5, -0.1, -0.5], result: 0xc1f3c1f3 },
+
+ // Subnormals
+ { inputs: [kValue.f32.subnormal.positive.max, 1, 1, 1], result: 0x7f7f7f00 },
+ { inputs: [kValue.f32.subnormal.negative.min, 1, 1, 1], result: 0x7f7f7f00 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack4x8snorm(inputs[0], inputs[1], inputs[2], inputs[3]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack4x8snorm(${inputs}) returned ${u32(got)}. Expected ${expect}`);
+ });
+
+g.test('pack4x8unorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0, 0, 0], result: 0x00000000 },
+ { inputs: [1, 0, 0, 0], result: 0x000000ff },
+ { inputs: [0, 1, 0, 0], result: 0x0000ff00 },
+ { inputs: [0, 0, 1, 0], result: 0x00ff0000 },
+ { inputs: [0, 0, 0, 1], result: 0xff000000 },
+ { inputs: [1, 1, 1, 1], result: 0xffffffff },
+ { inputs: [10, 10, 10, 10], result: 0xffffffff },
+ { inputs: [-1, -1, -1, -1], result: 0x00000000 },
+ { inputs: [-10, -10, -10, -10], result: 0x00000000 },
+ { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x1a1a1a1a },
+ { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x80808080 },
+ { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x801a801a },
+
+ // Subnormals
+ { inputs: [kValue.f32.subnormal.positive.max, 1, 1, 1], result: 0xffffff00 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack4x8unorm(inputs[0], inputs[1], inputs[2], inputs[3]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack4x8unorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts
new file mode 100644
index 0000000000..435d78ecd6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts
@@ -0,0 +1,3418 @@
+export const description = `
+F32Interval unit tests.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import { kValue } from '../webgpu/util/constants.js';
+import {
+ absInterval,
+ absoluteErrorInterval,
+ acosInterval,
+ acoshAlternativeInterval,
+ acoshPrimaryInterval,
+ additionInterval,
+ asinInterval,
+ asinhInterval,
+ atanInterval,
+ atan2Interval,
+ atanhInterval,
+ ceilInterval,
+ clampMedianInterval,
+ clampMinMaxInterval,
+ correctlyRoundedInterval,
+ cosInterval,
+ coshInterval,
+ crossInterval,
+ degreesInterval,
+ distanceInterval,
+ divisionInterval,
+ dotInterval,
+ expInterval,
+ exp2Interval,
+ F32Interval,
+ faceForwardIntervals,
+ floorInterval,
+ fmaInterval,
+ fractInterval,
+ IntervalBounds,
+ inverseSqrtInterval,
+ ldexpInterval,
+ lengthInterval,
+ logInterval,
+ log2Interval,
+ maxInterval,
+ minInterval,
+ mixImpreciseInterval,
+ mixPreciseInterval,
+ multiplicationInterval,
+ negationInterval,
+ normalizeInterval,
+ powInterval,
+ quantizeToF16Interval,
+ radiansInterval,
+ reflectInterval,
+ refractInterval,
+ remainderInterval,
+ roundInterval,
+ saturateInterval,
+ signInterval,
+ sinInterval,
+ sinhInterval,
+ smoothStepInterval,
+ sqrtInterval,
+ stepInterval,
+ subtractionInterval,
+ tanInterval,
+ tanhInterval,
+ toF32Vector,
+ truncInterval,
+ ulpInterval,
+ unpack2x16floatInterval,
+ unpack2x16snormInterval,
+ unpack2x16unormInterval,
+ unpack4x8snormInterval,
+ unpack4x8unormInterval,
+ modfInterval,
+ toF32Interval,
+} from '../webgpu/util/f32_interval.js';
+import { hexToF32, hexToF64, oneULP } from '../webgpu/util/math.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+/** Bounds indicating an expectation of an interval of all possible values */
+const kAny: IntervalBounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
+
+/** @returns a number N * ULP greater than the provided number */
+function plusNULP(x: number, n: number): number {
+ return x + n * oneULP(x);
+}
+
+/** @returns a number one ULP greater than the provided number */
+function plusOneULP(x: number): number {
+ return plusNULP(x, 1);
+}
+
+/** @returns a number N * ULP less than the provided number */
+function minusNULP(x: number, n: number): number {
+ return x - n * oneULP(x);
+}
+
+/** @returns a number one ULP less than the provided number */
+function minusOneULP(x: number): number {
+ return minusNULP(x, 1);
+}
+
+/** @returns the expected IntervalBounds adjusted by the given error function
+ *
+ * @param expected the bounds to be adjusted
+ * @param error error function to adjust the bounds via
+ */
+function applyError(expected: IntervalBounds, error: (n: number) => number): IntervalBounds {
+ if (expected !== kAny) {
+ const begin = expected[0];
+ const end = expected.length === 2 ? expected[1] : begin;
+ expected = [begin - error(begin), end + error(end)];
+ }
+
+ return expected;
+}
+
+interface ConstructorCase {
+ input: IntervalBounds;
+ expected: IntervalBounds;
+}
+
+g.test('constructor')
+ .paramsSubcasesOnly<ConstructorCase>(
+ // prettier-ignore
+ [
+ // Common cases
+ { input: [0, 10], expected: [0, 10]},
+ { input: [-5, 0], expected: [-5, 0]},
+ { input: [-5, 10], expected: [-5, 10]},
+ { input: [0], expected: [0]},
+ { input: [10], expected: [10]},
+ { input: [-5], expected: [-5]},
+
+ // Edges
+ { input: [0, kValue.f32.positive.max], expected: [0, kValue.f32.positive.max]},
+ { input: [kValue.f32.negative.min, 0], expected: [kValue.f32.negative.min, 0]},
+ { input: [kValue.f32.negative.min, kValue.f32.positive.max], expected: [kValue.f32.negative.min, kValue.f32.positive.max]},
+
+ // Out of range
+ { input: [0, 2 * kValue.f32.positive.max], expected: [0, 2 * kValue.f32.positive.max]},
+ { input: [2 * kValue.f32.negative.min, 0], expected: [2 * kValue.f32.negative.min, 0]},
+ { input: [2 * kValue.f32.negative.min, 2 * kValue.f32.positive.max], expected: [2 * kValue.f32.negative.min, 2 * kValue.f32.positive.max]},
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: [0, Number.POSITIVE_INFINITY]},
+ { input: [kValue.f32.infinity.negative, 0], expected: [Number.NEGATIVE_INFINITY, 0]},
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny},
+ ]
+ )
+ .fn(t => {
+ const i = new F32Interval(...t.params.input);
+ t.expect(
+ objectEquals(i.bounds(), t.params.expected),
+ `F32Interval([${t.params.input}]) returned ${i}. Expected [${t.params.expected}]`
+ );
+ });
+
+interface ContainsNumberCase {
+ bounds: IntervalBounds;
+ value: number;
+ expected: boolean;
+}
+
+g.test('contains_number')
+ .paramsSubcasesOnly<ContainsNumberCase>(
+ // prettier-ignore
+ [
+ // Common usage
+ { bounds: [0, 10], value: 0, expected: true },
+ { bounds: [0, 10], value: 10, expected: true },
+ { bounds: [0, 10], value: 5, expected: true },
+ { bounds: [0, 10], value: -5, expected: false },
+ { bounds: [0, 10], value: 50, expected: false },
+ { bounds: [0, 10], value: Number.NaN, expected: false },
+ { bounds: [-5, 10], value: 0, expected: true },
+ { bounds: [-5, 10], value: 10, expected: true },
+ { bounds: [-5, 10], value: 5, expected: true },
+ { bounds: [-5, 10], value: -5, expected: true },
+ { bounds: [-5, 10], value: -6, expected: false },
+ { bounds: [-5, 10], value: 50, expected: false },
+ { bounds: [-5, 10], value: -10, expected: false },
+
+ // Point
+ { bounds: [0], value: 0, expected: true },
+ { bounds: [0], value: 10, expected: false },
+ { bounds: [0], value: -1000, expected: false },
+ { bounds: [10], value: 10, expected: true },
+ { bounds: [10], value: 0, expected: false },
+ { bounds: [10], value: -10, expected: false },
+ { bounds: [10], value: 11, expected: false },
+
+ // Upper infinity
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.positive.min, expected: true },
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.positive.max, expected: true },
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.infinity.positive, expected: true },
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.negative.min, expected: false },
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.negative.max, expected: false },
+ { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.infinity.negative, expected: false },
+
+ // Lower infinity
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.positive.min, expected: false },
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.positive.max, expected: false },
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.infinity.positive, expected: false },
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.negative.min, expected: true },
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.negative.max, expected: true },
+ { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.infinity.negative, expected: true },
+
+ // Full infinity
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.positive.min, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.positive.max, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.infinity.positive, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.negative.min, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.negative.max, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.infinity.negative, expected: true },
+ { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: Number.NaN, expected: true },
+
+ // Maximum f32 boundary
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.positive.min, expected: true },
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.positive.max, expected: true },
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.infinity.positive, expected: false },
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.negative.min, expected: false },
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.negative.max, expected: false },
+ { bounds: [0, kValue.f32.positive.max], value: kValue.f32.infinity.negative, expected: false },
+
+ // Minimum f32 boundary
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.positive.min, expected: false },
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.positive.max, expected: false },
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.infinity.positive, expected: false },
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.negative.min, expected: true },
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.negative.max, expected: true },
+ { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.infinity.negative, expected: false },
+
+ // Out of range high
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.positive.min, expected: true },
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.positive.max, expected: true },
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.infinity.positive, expected: false },
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.negative.min, expected: false },
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.negative.max, expected: false },
+ { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.infinity.negative, expected: false },
+
+ // Out of range low
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.positive.min, expected: false },
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.positive.max, expected: false },
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.infinity.positive, expected: false },
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.negative.min, expected: true },
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.negative.max, expected: true },
+ { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.infinity.negative, expected: false },
+
+ // Subnormals
+ { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.positive.min, expected: true },
+ { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.positive.max, expected: true },
+ { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.negative.min, expected: false },
+ { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.negative.max, expected: false },
+ { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.positive.min, expected: false },
+ { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.positive.max, expected: false },
+ { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.negative.min, expected: true },
+ { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.negative.max, expected: true },
+ { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.positive.min, expected: true },
+ { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.positive.max, expected: false },
+ { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.negative.min, expected: false },
+ { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.negative.max, expected: false },
+ { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.positive.min, expected: false },
+ { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.positive.max, expected: false },
+ { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.negative.min, expected: false },
+ { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.negative.max, expected: true },
+ ]
+ )
+ .fn(t => {
+ const i = new F32Interval(...t.params.bounds);
+ const value = t.params.value;
+ const expected = t.params.expected;
+
+ const got = i.contains(value);
+ t.expect(expected === got, `${i}.contains(${value}) returned ${got}. Expected ${expected}`);
+ });
+
+interface ContainsIntervalCase {
+ lhs: IntervalBounds;
+ rhs: IntervalBounds;
+ expected: boolean;
+}
+
+g.test('contains_interval')
+ .paramsSubcasesOnly<ContainsIntervalCase>(
+ // prettier-ignore
+ [
+ // Common usage
+ { lhs: [-10, 10], rhs: [0], expected: true},
+ { lhs: [-10, 10], rhs: [-1, 0], expected: true},
+ { lhs: [-10, 10], rhs: [0, 2], expected: true},
+ { lhs: [-10, 10], rhs: [-1, 2], expected: true},
+ { lhs: [-10, 10], rhs: [0, 10], expected: true},
+ { lhs: [-10, 10], rhs: [-10, 2], expected: true},
+ { lhs: [-10, 10], rhs: [-10, 10], expected: true},
+ { lhs: [-10, 10], rhs: [-100, 10], expected: false},
+
+ // Upper infinity
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [0], expected: true},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [-1, 0], expected: false},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [0, 1], expected: true},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [0, kValue.f32.positive.max], expected: true},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [0, kValue.f32.infinity.positive], expected: true},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [100, kValue.f32.infinity.positive], expected: true},
+ { lhs: [0, kValue.f32.infinity.positive], rhs: [Number.NEGATIVE_INFINITY, kValue.f32.infinity.positive], expected: false},
+
+ // Lower infinity
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [-1, 0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.negative.min, 0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [0, 1], expected: false},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, 0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: true},
+ { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false},
+
+ // Full infinity
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [-1, 0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0, 1], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0, kValue.f32.infinity.positive], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [100, kValue.f32.infinity.positive], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, 0], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, -100 ], expected: true},
+ { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: true},
+
+ // Maximum f32 boundary
+ { lhs: [0, kValue.f32.positive.max], rhs: [0], expected: true},
+ { lhs: [0, kValue.f32.positive.max], rhs: [-1, 0], expected: false},
+ { lhs: [0, kValue.f32.positive.max], rhs: [0, 1], expected: true},
+ { lhs: [0, kValue.f32.positive.max], rhs: [0, kValue.f32.positive.max], expected: true},
+ { lhs: [0, kValue.f32.positive.max], rhs: [0, kValue.f32.infinity.positive], expected: false},
+ { lhs: [0, kValue.f32.positive.max], rhs: [100, kValue.f32.infinity.positive], expected: false},
+ { lhs: [0, kValue.f32.positive.max], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false},
+
+ // Minimum f32 boundary
+ { lhs: [kValue.f32.negative.min, 0], rhs: [0, 0], expected: true},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [-1, 0], expected: true},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.negative.min, 0], expected: true},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [0, 1], expected: false},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, 0], expected: false},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: false},
+ { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false},
+
+ // Out of range high
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0], expected: true},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [-1, 0], expected: false},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, 1], expected: true},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, kValue.f32.positive.max], expected: true},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, kValue.f32.infinity.positive], expected: false},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [100, kValue.f32.infinity.positive], expected: false},
+ { lhs: [0, 2 * kValue.f32.positive.max], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false},
+
+ // Out of range low
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [0], expected: true},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [-1, 0], expected: true},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.negative.min, 0], expected: true},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [0, 1], expected: false},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, 0], expected: false},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: false},
+ { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false},
+ ]
+ )
+ .fn(t => {
+ const lhs = new F32Interval(...t.params.lhs);
+ const rhs = new F32Interval(...t.params.rhs);
+ const expected = t.params.expected;
+
+ const got = lhs.contains(rhs);
+ t.expect(expected === got, `${lhs}.contains(${rhs}) returned ${got}. Expected ${expected}`);
+ });
+
+interface SpanCase {
+ intervals: IntervalBounds[];
+ expected: IntervalBounds;
+}
+
+g.test('span')
+ .paramsSubcasesOnly<SpanCase>(
+ // prettier-ignore
+ [
+ // Single Intervals
+ { intervals: [[0, 10]], expected: [0, 10]},
+ { intervals: [[0, kValue.f32.positive.max]], expected: [0, kValue.f32.positive.max]},
+ { intervals: [[0, kValue.f32.positive.nearest_max]], expected: [0, kValue.f32.positive.nearest_max]},
+ { intervals: [[0, kValue.f32.infinity.positive]], expected: [0, Number.POSITIVE_INFINITY]},
+ { intervals: [[kValue.f32.negative.min, 0]], expected: [kValue.f32.negative.min, 0]},
+ { intervals: [[kValue.f32.negative.nearest_min, 0]], expected: [kValue.f32.negative.nearest_min, 0]},
+ { intervals: [[kValue.f32.infinity.negative, 0]], expected: [Number.NEGATIVE_INFINITY, 0]},
+
+ // Double Intervals
+ { intervals: [[0, 1], [2, 5]], expected: [0, 5]},
+ { intervals: [[2, 5], [0, 1]], expected: [0, 5]},
+ { intervals: [[0, 2], [1, 5]], expected: [0, 5]},
+ { intervals: [[0, 5], [1, 2]], expected: [0, 5]},
+ { intervals: [[kValue.f32.infinity.negative, 0], [0, kValue.f32.infinity.positive]], expected: kAny},
+
+ // Multiple Intervals
+ { intervals: [[0, 1], [2, 3], [4, 5]], expected: [0, 5]},
+ { intervals: [[0, 1], [4, 5], [2, 3]], expected: [0, 5]},
+ { intervals: [[0, 1], [0, 1], [0, 1]], expected: [0, 1]},
+ ]
+ )
+ .fn(t => {
+ const intervals = t.params.intervals.map(toF32Interval);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = F32Interval.span(...intervals);
+ t.expect(
+ objectEquals(got, expected),
+ `span({${intervals}}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface CorrectlyRoundedCase {
+ value: number;
+ expected: IntervalBounds;
+}
+
+g.test('correctlyRoundedInterval')
+ .paramsSubcasesOnly<CorrectlyRoundedCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f32.infinity.positive, expected: kAny },
+ { value: kValue.f32.infinity.negative, expected: kAny },
+ { value: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { value: kValue.f32.positive.min, expected: [kValue.f32.positive.min] },
+ { value: kValue.f32.negative.max, expected: [kValue.f32.negative.max] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.subnormal.positive.min, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: kValue.f32.subnormal.positive.max, expected: [0, kValue.f32.subnormal.positive.max] },
+ { value: kValue.f32.subnormal.negative.min, expected: [kValue.f32.subnormal.negative.min, 0] },
+ { value: kValue.f32.subnormal.negative.max, expected: [kValue.f32.subnormal.negative.max, 0] },
+
+ // 64-bit subnormals
+ { value: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000002), expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x800fffff, 0xffffffff), expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), expected: [kValue.f32.subnormal.negative.max, 0] },
+
+ // 32-bit normals
+ { value: 0, expected: [0, 0] },
+ { value: hexToF32(0x03800000), expected: [hexToF32(0x03800000)] },
+ { value: hexToF32(0x03800001), expected: [hexToF32(0x03800001)] },
+ { value: hexToF32(0x83800000), expected: [hexToF32(0x83800000)] },
+ { value: hexToF32(0x83800001), expected: [hexToF32(0x83800001)] },
+
+ // 64-bit normals
+ { value: hexToF64(0x3ff00000, 0x00000001), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] },
+ { value: hexToF64(0x3ff00000, 0x00000002), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] },
+ { value: hexToF64(0x3ff00010, 0x00000010), expected: [hexToF32(0x3f800080), hexToF32(0x3f800081)] },
+ { value: hexToF64(0x3ff00020, 0x00000020), expected: [hexToF32(0x3f800100), hexToF32(0x3f800101)] },
+ { value: hexToF64(0xbff00000, 0x00000001), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] },
+ { value: hexToF64(0xbff00000, 0x00000002), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] },
+ { value: hexToF64(0xbff00010, 0x00000010), expected: [hexToF32(0xbf800081), hexToF32(0xbf800080)] },
+ { value: hexToF64(0xbff00020, 0x00000020), expected: [hexToF32(0xbf800101), hexToF32(0xbf800100)] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = correctlyRoundedInterval(t.params.value);
+ t.expect(
+ objectEquals(expected, got),
+ `correctlyRoundedInterval(${t.params.value}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface AbsoluteErrorCase {
+ value: number;
+ error: number;
+ expected: IntervalBounds;
+}
+
+g.test('absoluteErrorInterval')
+ .paramsSubcasesOnly<AbsoluteErrorCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f32.infinity.positive, error: 0, expected: kAny },
+ { value: kValue.f32.infinity.positive, error: 2 ** -11, expected: kAny },
+ { value: kValue.f32.infinity.positive, error: 1, expected: kAny },
+ { value: kValue.f32.infinity.negative, error: 0, expected: kAny },
+ { value: kValue.f32.infinity.negative, error: 2 ** -11, expected: kAny },
+ { value: kValue.f32.infinity.negative, error: 1, expected: kAny },
+ { value: kValue.f32.positive.max, error: 0, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.positive.max, error: 2 ** -11, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.positive.max, error: kValue.f32.positive.max, expected: kAny },
+ { value: kValue.f32.positive.min, error: 0, expected: [kValue.f32.positive.min] },
+ { value: kValue.f32.positive.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.positive.min, error: 1, expected: [-1, 1] },
+ { value: kValue.f32.negative.min, error: 0, expected: [kValue.f32.negative.min] },
+ { value: kValue.f32.negative.min, error: 2 ** -11, expected: [kValue.f32.negative.min] },
+ { value: kValue.f32.negative.min, error: kValue.f32.positive.max, expected: kAny },
+ { value: kValue.f32.negative.max, error: 0, expected: [kValue.f32.negative.max] },
+ { value: kValue.f32.negative.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.negative.max, error: 1, expected: [-1, 1] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.subnormal.positive.max, error: 0, expected: [0, kValue.f32.subnormal.positive.max] },
+ { value: kValue.f32.subnormal.positive.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.subnormal.positive.max, error: 1, expected: [-1, 1] },
+ { value: kValue.f32.subnormal.positive.min, error: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: kValue.f32.subnormal.positive.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.subnormal.positive.min, error: 1, expected: [-1, 1] },
+ { value: kValue.f32.subnormal.negative.min, error: 0, expected: [kValue.f32.subnormal.negative.min, 0] },
+ { value: kValue.f32.subnormal.negative.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.subnormal.negative.min, error: 1, expected: [-1, 1] },
+ { value: kValue.f32.subnormal.negative.max, error: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: kValue.f32.subnormal.negative.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: kValue.f32.subnormal.negative.max, error: 1, expected: [-1, 1] },
+
+ // 64-bit subnormals
+ { value: hexToF64(0x00000000, 0x00000001), error: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000001), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: hexToF64(0x00000000, 0x00000001), error: 1, expected: [-1, 1] },
+ { value: hexToF64(0x00000000, 0x00000002), error: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000002), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: hexToF64(0x00000000, 0x00000002), error: 1, expected: [-1, 1] },
+ { value: hexToF64(0x800fffff, 0xffffffff), error: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xffffffff), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: hexToF64(0x800fffff, 0xffffffff), error: 1, expected: [-1, 1] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), error: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), error: 1, expected: [-1, 1] },
+
+ // Zero
+ { value: 0, error: 0, expected: [0] },
+ { value: 0, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] },
+ { value: 0, error: 1, expected: [-1, 1] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = absoluteErrorInterval(t.params.value, t.params.error);
+ t.expect(
+ objectEquals(expected, got),
+ `absoluteErrorInterval(${t.params.value}, ${t.params.error}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface ULPCase {
+ value: number;
+ num_ulp: number;
+ expected: IntervalBounds;
+}
+
+g.test('ulpInterval')
+ .paramsSubcasesOnly<ULPCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f32.infinity.positive, num_ulp: 0, expected: kAny },
+ { value: kValue.f32.infinity.positive, num_ulp: 1, expected: kAny },
+ { value: kValue.f32.infinity.positive, num_ulp: 4096, expected: kAny },
+ { value: kValue.f32.infinity.negative, num_ulp: 0, expected: kAny },
+ { value: kValue.f32.infinity.negative, num_ulp: 1, expected: kAny },
+ { value: kValue.f32.infinity.negative, num_ulp: 4096, expected: kAny },
+ { value: kValue.f32.positive.max, num_ulp: 0, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.positive.max, num_ulp: 1, expected: kAny },
+ { value: kValue.f32.positive.max, num_ulp: 4096, expected: kAny },
+ { value: kValue.f32.positive.min, num_ulp: 0, expected: [kValue.f32.positive.min] },
+ { value: kValue.f32.positive.min, num_ulp: 1, expected: [0, plusOneULP(kValue.f32.positive.min)] },
+ { value: kValue.f32.positive.min, num_ulp: 4096, expected: [0, plusNULP(kValue.f32.positive.min, 4096)] },
+ { value: kValue.f32.negative.min, num_ulp: 0, expected: [kValue.f32.negative.min] },
+ { value: kValue.f32.negative.min, num_ulp: 1, expected: kAny },
+ { value: kValue.f32.negative.min, num_ulp: 4096, expected: kAny },
+ { value: kValue.f32.negative.max, num_ulp: 0, expected: [kValue.f32.negative.max] },
+ { value: kValue.f32.negative.max, num_ulp: 1, expected: [minusOneULP(kValue.f32.negative.max), 0] },
+ { value: kValue.f32.negative.max, num_ulp: 4096, expected: [minusNULP(kValue.f32.negative.max, 4096), 0] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.subnormal.positive.max, num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.max] },
+ { value: kValue.f32.subnormal.positive.max, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.max)] },
+ { value: kValue.f32.subnormal.positive.max, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.max, 4096)] },
+ { value: kValue.f32.subnormal.positive.min, num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: kValue.f32.subnormal.positive.min, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] },
+ { value: kValue.f32.subnormal.positive.min, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] },
+ { value: kValue.f32.subnormal.negative.min, num_ulp: 0, expected: [kValue.f32.subnormal.negative.min, 0] },
+ { value: kValue.f32.subnormal.negative.min, num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.min), plusOneULP(0)] },
+ { value: kValue.f32.subnormal.negative.min, num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.min, 4096), plusNULP(0, 4096)] },
+ { value: kValue.f32.subnormal.negative.max, num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: kValue.f32.subnormal.negative.max, num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] },
+ { value: kValue.f32.subnormal.negative.max, num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] },
+
+ // 64-bit subnormals
+ { value: hexToF64(0x00000000, 0x00000001), num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000001), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] },
+ { value: hexToF64(0x00000000, 0x00000001), num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] },
+ { value: hexToF64(0x00000000, 0x00000002), num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000002), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] },
+ { value: hexToF64(0x00000000, 0x00000002), num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] },
+ { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] },
+ { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] },
+
+ // Zero
+ { value: 0, num_ulp: 0, expected: [0] },
+ { value: 0, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(0)] },
+ { value: 0, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(0, 4096)] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = ulpInterval(t.params.value, t.params.num_ulp);
+ t.expect(
+ objectEquals(expected, got),
+ `ulpInterval(${t.params.value}, ${t.params.num_ulp}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface PointToIntervalCase {
+ input: number;
+ expected: IntervalBounds;
+}
+
+g.test('absInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Common usages
+ { input: 1, expected: [1] },
+ { input: -1, expected: [1] },
+ { input: 0.1, expected: [hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)] },
+ { input: -0.1, expected: [hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)] },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.negative.max, expected: [kValue.f32.positive.min] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0, kValue.f32.subnormal.positive.min] },
+
+ // 64-bit subnormals
+ { input: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: hexToF64(0x800fffff, 0xffffffff), expected: [0, kValue.f32.subnormal.positive.min] },
+
+ // Zero
+ { input: 0, expected: [0]},
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = absInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `absInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('acosInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ //
+ // The acceptance interval @ x = -1 and 1 is kAny, because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inverseqrt
+ // The acceptance interval @ x = 0 is kAny, because atan2 is not well defined/implemented at 0.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: kAny },
+ { input: -1/2, expected: [hexToF32(0x4005fa91), hexToF32(0x40061a94)] }, // ~2π/3
+ { input: 0, expected: kAny },
+ { input: 1/2, expected: [hexToF32(0x3f85fa8f), hexToF32(0x3f861a94)] }, // ~π/3
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = acosInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `acosInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('acoshAlternativeInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 1, expected: kAny }, // 1/0 occurs in inverseSqrt in this formulation
+ { input: 1.1, expected: [hexToF64(0x3fdc6368, 0x80000000), hexToF64(0x3fdc636f, 0x20000000)] }, // ~0.443..., differs from the primary in the later digits
+ { input: 10, expected: [hexToF64(0x4007f21e, 0x40000000), hexToF64(0x4007f21f, 0x60000000)] }, // ~2.993...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = acoshAlternativeInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `acoshInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('acoshPrimaryInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 1, expected: kAny }, // 1/0 occurs in inverseSqrt in this formulation
+ { input: 1.1, expected: [hexToF64(0x3fdc6368, 0x20000000), hexToF64(0x3fdc636f, 0x80000000)] }, // ~0.443..., differs from the alternative in the later digits
+ { input: 10, expected: [hexToF64(0x4007f21e, 0x40000000), hexToF64(0x4007f21f, 0x60000000)] }, // ~2.993...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = acoshPrimaryInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `acoshInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('asinInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ //
+ // The acceptance interval @ x = -1 and 1 is kAny, because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inversqrt
+ // The acceptance interval @ x = 0 is kAny, because atan2 is not well defined/implemented at 0.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: kAny },
+ { input: -1/2, expected: [hexToF32(0xbf061a96), hexToF32(0xbf05fa8e)] }, // ~-π/6
+ { input: 0, expected: kAny },
+ { input: 1/2, expected: [hexToF32(0x3f05fa8e), hexToF32(0x3f061a96)] }, // ~π/6
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = asinInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `asinInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('asinhInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: [hexToF64(0xbfec343a, 0x80000000), hexToF64(0xbfec3432, 0x80000000)] }, // ~-0.88137...
+ { input: 0, expected: [hexToF64(0xbeaa0000, 0x20000000), hexToF64(0x3eb1ffff, 0xd0000000)] }, // ~0
+ { input: 1, expected: [hexToF64(0x3fec3435, 0x40000000), hexToF64(0x3fec3437, 0x80000000)] }, // ~0.88137...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = asinhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `asinhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('atanInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: hexToF32(0xbfddb3d7), expected: [kValue.f32.negative.pi.third, plusOneULP(kValue.f32.negative.pi.third)] }, // x = -√3
+ { input: -1, expected: [kValue.f32.negative.pi.quarter, plusOneULP(kValue.f32.negative.pi.quarter)] },
+ { input: hexToF32(0xbf13cd3a), expected: [kValue.f32.negative.pi.sixth, plusOneULP(kValue.f32.negative.pi.sixth)] }, // x = -1/√3
+ { input: 0, expected: [0] },
+ { input: hexToF32(0x3f13cd3a), expected: [minusOneULP(kValue.f32.positive.pi.sixth), kValue.f32.positive.pi.sixth] }, // x = 1/√3
+ { input: 1, expected: [minusOneULP(kValue.f32.positive.pi.quarter), kValue.f32.positive.pi.quarter] },
+ { input: hexToF32(0x3fddb3d7), expected: [minusOneULP(kValue.f32.positive.pi.third), kValue.f32.positive.pi.third] }, // x = √3
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (n: number): number => {
+ return 4096 * oneULP(n);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = atanInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `atanInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('atanhInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: kAny },
+ { input: -0.1, expected: [hexToF64(0xbfb9af9a, 0x60000000), hexToF64(0xbfb9af8c, 0xc0000000)] }, // ~-0.1003...
+ { input: 0, expected: [hexToF64(0xbe960000, 0x20000000), hexToF64(0x3e980000, 0x00000000)] }, // ~0
+ { input: 0.1, expected: [hexToF64(0x3fb9af8b, 0x80000000), hexToF64(0x3fb9af9b, 0x00000000)] }, // ~0.1003...
+ { input: 1, expected: kAny },
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = atanhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `atanhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('ceilInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [1] },
+ { input: 0.9, expected: [1] },
+ { input: 1.0, expected: [1] },
+ { input: 1.1, expected: [2] },
+ { input: 1.9, expected: [2] },
+ { input: -0.1, expected: [0] },
+ { input: -0.9, expected: [0] },
+ { input: -1.0, expected: [-1] },
+ { input: -1.1, expected: [-1] },
+ { input: -1.9, expected: [-1] },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.positive.min, expected: [1] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { input: kValue.f32.negative.max, expected: [0] },
+ { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] },
+ { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0, 1] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0, 1] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = ceilInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `ceilInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('cosInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // This test does not include some common cases. i.e. f(x = π/2) = 0, because the difference between true x
+ // and x as a f32 is sufficiently large, such that the high slope of f @ x causes the results to be substantially
+ // different, so instead of getting 0 you get a value on the order of 10^-8 away from 0, thus difficult to express
+ // in a human readable manner.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f32.negative.pi.whole, expected: [-1, plusOneULP(-1)] },
+ { input: kValue.f32.negative.pi.third, expected: [minusOneULP(1/2), 1/2] },
+ { input: 0, expected: [1, 1] },
+ { input: kValue.f32.positive.pi.third, expected: [minusOneULP(1/2), 1/2] },
+ { input: kValue.f32.positive.pi.whole, expected: [-1, plusOneULP(-1)] },
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (_: number): number => {
+ return 2 ** -11;
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = cosInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `cosInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('coshInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: [ hexToF32(0x3fc583a4), hexToF32(0x3fc583b1)] }, // ~1.1543...
+ { input: 0, expected: [hexToF32(0x3f7ffffd), hexToF32(0x3f800002)] }, // ~1
+ { input: 1, expected: [ hexToF32(0x3fc583a4), hexToF32(0x3fc583b1)] }, // ~1.1543...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = coshInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `coshInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('degreesInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f32.negative.pi.whole, expected: [minusOneULP(-180), plusOneULP(-180)] },
+ { input: kValue.f32.negative.pi.three_quarters, expected: [minusOneULP(-135), plusOneULP(-135)] },
+ { input: kValue.f32.negative.pi.half, expected: [minusOneULP(-90), plusOneULP(-90)] },
+ { input: kValue.f32.negative.pi.third, expected: [minusOneULP(-60), plusOneULP(-60)] },
+ { input: kValue.f32.negative.pi.quarter, expected: [minusOneULP(-45), plusOneULP(-45)] },
+ { input: kValue.f32.negative.pi.sixth, expected: [minusOneULP(-30), plusOneULP(-30)] },
+ { input: 0, expected: [0] },
+ { input: kValue.f32.positive.pi.sixth, expected: [minusOneULP(30), plusOneULP(30)] },
+ { input: kValue.f32.positive.pi.quarter, expected: [minusOneULP(45), plusOneULP(45)] },
+ { input: kValue.f32.positive.pi.third, expected: [minusOneULP(60), plusOneULP(60)] },
+ { input: kValue.f32.positive.pi.half, expected: [minusOneULP(90), plusOneULP(90)] },
+ { input: kValue.f32.positive.pi.three_quarters, expected: [minusOneULP(135), plusOneULP(135)] },
+ { input: kValue.f32.positive.pi.whole, expected: [minusOneULP(180), plusOneULP(180)] },
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = degreesInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `degreesInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('expInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: 0, expected: [1] },
+ { input: 1, expected: [kValue.f32.positive.e, plusOneULP(kValue.f32.positive.e)] },
+ { input: 89, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (x: number): number => {
+ const n = 3 + 2 * Math.abs(t.params.input);
+ return n * oneULP(x);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = expInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `expInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('exp2Interval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: 0, expected: [1] },
+ { input: 1, expected: [2] },
+ { input: 128, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (x: number): number => {
+ const n = 3 + 2 * Math.abs(t.params.input);
+ return n * oneULP(x);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = exp2Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `exp2Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('floorInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [0] },
+ { input: 0.9, expected: [0] },
+ { input: 1.0, expected: [1] },
+ { input: 1.1, expected: [1] },
+ { input: 1.9, expected: [1] },
+ { input: -0.1, expected: [-1] },
+ { input: -0.9, expected: [-1] },
+ { input: -1.0, expected: [-1] },
+ { input: -1.1, expected: [-2] },
+ { input: -1.9, expected: [-2] },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.positive.min, expected: [0] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { input: kValue.f32.negative.max, expected: [-1] },
+ { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] },
+ { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.min, expected: [-1, 0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [-1, 0] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = floorInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `floorInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('fractInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: 0.9, expected: [hexToF32(0x3f666666), plusOneULP(hexToF32(0x3f666666))] }, // ~0.9
+ { input: 1.0, expected: [0] },
+ { input: 1.1, expected: [hexToF64(0x3fb99998, 0x00000000), hexToF64(0x3fb9999a, 0x00000000)] }, // ~0.1
+ { input: -0.1, expected: [hexToF32(0x3f666666), plusOneULP(hexToF32(0x3f666666))] }, // ~0.9
+ { input: -0.9, expected: [hexToF64(0x3fb99999, 0x00000000), hexToF64(0x3fb9999a, 0x00000000)] }, // ~0.1
+ { input: -1.0, expected: [0] },
+ { input: -1.1, expected: [hexToF64(0x3feccccc, 0xc0000000), hexToF64(0x3feccccd, 0x00000000), ] }, // ~0.9
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [0] },
+ { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min, kValue.f32.positive.min] },
+ { input: kValue.f32.negative.min, expected: [0] },
+ { input: kValue.f32.negative.max, expected: [kValue.f32.positive.less_than_one, 1.0] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = fractInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `fractInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('inverseSqrtInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 0.04, expected: [minusOneULP(5), plusOneULP(5)] },
+ { input: 1, expected: [1] },
+ { input: 100, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: kValue.f32.positive.max, expected: [hexToF32(0x1f800000), plusNULP(hexToF32(0x1f800000), 2)] }, // ~5.421...e-20, i.e. 1/√max f32
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (n: number): number => {
+ return 2 * oneULP(n);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = inverseSqrtInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `inverseSqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('lengthIntervalScalar')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ //
+ // length(0) = kAny, because length uses sqrt, which is defined as 1/inversesqrt
+ {input: 0, expected: kAny },
+ {input: 1.0, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: -1.0, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: 0.1, expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ {input: -0.1, expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ {input: 10.0, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+ {input: -10.0, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+
+ // Subnormal Cases
+ { input: kValue.f32.subnormal.negative.min, expected: kAny },
+ { input: kValue.f32.subnormal.negative.max, expected: kAny },
+ { input: kValue.f32.subnormal.positive.min, expected: kAny },
+ { input: kValue.f32.subnormal.positive.max, expected: kAny },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f32.negative.max, expected: kAny },
+ { input: kValue.f32.positive.min, expected: kAny },
+ { input: kValue.f32.positive.max, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = lengthInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `lengthInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('logInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 1, expected: [0] },
+ { input: kValue.f32.positive.e, expected: [minusOneULP(1), 1] },
+ { input: kValue.f32.positive.max, expected: [minusOneULP(hexToF32(0x42b17218)), hexToF32(0x42b17218)] }, // ~88.72...
+ ]
+ )
+ .fn(t => {
+ const error = (n: number): number => {
+ if (t.params.input >= 0.5 && t.params.input <= 2.0) {
+ return 2 ** -21;
+ }
+ return 3 * oneULP(n);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = logInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `logInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('log2Interval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 1, expected: [0] },
+ { input: 2, expected: [1] },
+ { input: kValue.f32.positive.max, expected: [minusOneULP(128), 128] },
+ ]
+ )
+ .fn(t => {
+ const error = (n: number): number => {
+ if (t.params.input >= 0.5 && t.params.input <= 2.0) {
+ return 2 ** -21;
+ }
+ return 3 * oneULP(n);
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = log2Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `log2Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('negationInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: 1.0, expected: [-1.0] },
+ { input: 1.9, expected: [hexToF32(0xbff33334), plusOneULP(hexToF32(0xbff33334))] }, // ~-1.9
+ { input: -0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: -1.0, expected: [1] },
+ { input: -1.9, expected: [minusOneULP(hexToF32(0x3ff33334)), hexToF32(0x3ff33334)] }, // ~1.9
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.negative.min] },
+ { input: kValue.f32.positive.min, expected: [kValue.f32.negative.max] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.negative.max, expected: [kValue.f32.positive.min] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: kValue.f32.subnormal.positive.min, expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0, kValue.f32.subnormal.positive.min] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = negationInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `negationInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('quantizeToF16Interval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f16.negative.min, expected: [kValue.f16.negative.min] },
+ { input: -1, expected: [-1] },
+ { input: -0.1, expected: [hexToF32(0xbdcce000), hexToF32(0xbdccc000)] }, // ~-0.1
+ { input: kValue.f16.negative.max, expected: [kValue.f16.negative.max] },
+ { input: kValue.f16.subnormal.negative.min, expected: [kValue.f16.subnormal.negative.min, 0] },
+ { input: kValue.f16.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max, 0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max, 0] },
+ { input: 0, expected: [0] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0, kValue.f16.subnormal.positive.min] },
+ { input: kValue.f16.subnormal.positive.min, expected: [0, kValue.f16.subnormal.positive.min] },
+ { input: kValue.f16.subnormal.positive.max, expected: [0, kValue.f16.subnormal.positive.max] },
+ { input: kValue.f16.positive.min, expected: [kValue.f16.positive.min] },
+ { input: 0.1, expected: [hexToF32(0x3dccc000), hexToF32(0x3dcce000)] }, // ~0.1
+ { input: 1, expected: [1] },
+ { input: kValue.f16.positive.max, expected: [kValue.f16.positive.max] },
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = quantizeToF16Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `quantizeToF16Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('radiansInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: -180, expected: [minusOneULP(kValue.f32.negative.pi.whole), plusOneULP(kValue.f32.negative.pi.whole)] },
+ { input: -135, expected: [minusOneULP(kValue.f32.negative.pi.three_quarters), plusOneULP(kValue.f32.negative.pi.three_quarters)] },
+ { input: -90, expected: [minusOneULP(kValue.f32.negative.pi.half), plusOneULP(kValue.f32.negative.pi.half)] },
+ { input: -60, expected: [minusOneULP(kValue.f32.negative.pi.third), plusOneULP(kValue.f32.negative.pi.third)] },
+ { input: -45, expected: [minusOneULP(kValue.f32.negative.pi.quarter), plusOneULP(kValue.f32.negative.pi.quarter)] },
+ { input: -30, expected: [minusOneULP(kValue.f32.negative.pi.sixth), plusOneULP(kValue.f32.negative.pi.sixth)] },
+ { input: 0, expected: [0] },
+ { input: 30, expected: [minusOneULP(kValue.f32.positive.pi.sixth), plusOneULP(kValue.f32.positive.pi.sixth)] },
+ { input: 45, expected: [minusOneULP(kValue.f32.positive.pi.quarter), plusOneULP(kValue.f32.positive.pi.quarter)] },
+ { input: 60, expected: [minusOneULP(kValue.f32.positive.pi.third), plusOneULP(kValue.f32.positive.pi.third)] },
+ { input: 90, expected: [minusOneULP(kValue.f32.positive.pi.half), plusOneULP(kValue.f32.positive.pi.half)] },
+ { input: 135, expected: [minusOneULP(kValue.f32.positive.pi.three_quarters), plusOneULP(kValue.f32.positive.pi.three_quarters)] },
+ { input: 180, expected: [minusOneULP(kValue.f32.positive.pi.whole), plusOneULP(kValue.f32.positive.pi.whole)] },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = radiansInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `radiansInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('roundInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [0] },
+ { input: 0.5, expected: [0] }, // Testing tie breaking
+ { input: 0.9, expected: [1] },
+ { input: 1.0, expected: [1] },
+ { input: 1.1, expected: [1] },
+ { input: 1.5, expected: [2] }, // Testing tie breaking
+ { input: 1.9, expected: [2] },
+ { input: -0.1, expected: [0] },
+ { input: -0.5, expected: [0] }, // Testing tie breaking
+ { input: -0.9, expected: [-1] },
+ { input: -1.0, expected: [-1] },
+ { input: -1.1, expected: [-1] },
+ { input: -1.5, expected: [-2] }, // Testing tie breaking
+ { input: -1.9, expected: [-2] },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.positive.min, expected: [0] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { input: kValue.f32.negative.max, expected: [0] },
+ { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] },
+ { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = roundInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `roundInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('saturateInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Normals
+ { input: 0, expected: [0] },
+ { input: 1, expected: [1.0] },
+ { input: -0.1, expected: [0] },
+ { input: -1, expected: [0] },
+ { input: -10, expected: [0] },
+ { input: 0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: 10, expected: [1.0] },
+ { input: 11.1, expected: [1.0] },
+ { input: kValue.f32.positive.max, expected: [1.0] },
+ { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min] },
+ { input: kValue.f32.negative.max, expected: [0.0] },
+ { input: kValue.f32.negative.min, expected: [0.0] },
+
+ // Subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0.0, kValue.f32.subnormal.positive.max] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0.0, kValue.f32.subnormal.positive.min] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0.0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0.0] },
+
+ // Infinities
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = saturateInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `saturationInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('signInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: [-1] },
+ { input: -10, expected: [-1] },
+ { input: -1, expected: [-1] },
+ { input: -0.1, expected: [-1] },
+ { input: kValue.f32.negative.max, expected: [-1] },
+ { input: kValue.f32.subnormal.negative.min, expected: [-1, 0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [-1, 0] },
+ { input: 0, expected: [0] },
+ { input: kValue.f32.subnormal.positive.max, expected: [0, 1] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0, 1] },
+ { input: kValue.f32.positive.min, expected: [1] },
+ { input: 0.1, expected: [1] },
+ { input: 1, expected: [1] },
+ { input: 10, expected: [1] },
+ { input: kValue.f32.positive.max, expected: [1] },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = signInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `signInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('sinInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // This test does not include some common cases, i.e. f(x = -π|π) = 0, because the difference between true x and x
+ // as a f32 is sufficiently large, such that the high slope of f @ x causes the results to be substantially
+ // different, so instead of getting 0 you get a value on the order of 10^-8 away from it, thus difficult to
+ // express in a human readable manner.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f32.negative.pi.half, expected: [-1, plusOneULP(-1)] },
+ { input: 0, expected: [0] },
+ { input: kValue.f32.positive.pi.half, expected: [minusOneULP(1), 1] },
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (_: number): number => {
+ return 2 ** -11;
+ };
+
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = sinInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `sinInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('sinhInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: [ hexToF32(0xbf966d05), hexToF32(0xbf966cf8)] }, // ~-1.175...
+ { input: 0, expected: [hexToF32(0xb4600000), hexToF32(0x34600000)] }, // ~0
+ { input: 1, expected: [ hexToF32(0x3f966cf8), hexToF32(0x3f966d05)] }, // ~1.175...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = sinhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `sinhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('sqrtInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: -1, expected: kAny },
+ { input: 0, expected: kAny },
+ { input: 0.01, expected: [hexToF64(0x3fb99998, 0xb0000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: 1, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: 4, expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ { input: 100, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = sqrtInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `sqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('tanInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // All of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form. Some easy looking cases like f(x = -π|π) = 0 are actually quite difficult. This is because the interval
+ // is calculated from the results of sin(x)/cos(x), which becomes very messy at x = -π|π, since π is irrational,
+ // thus does not have an exact representation as a f32.
+ // Even at 0, which has a precise f32 value, there is still the problem that result of sin(0) and cos(0) will be
+ // intervals due to the inherited nature of errors, so the proper interval will be an interval calculated from
+ // dividing an interval by another interval and applying an error function to that. This complexity is why the
+ // entire interval framework was developed.
+ // The examples here have been manually traced to confirm the expectation values are correct.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: kValue.f32.negative.pi.whole, expected: [hexToF64(0xbf4002bc, 0x90000000), hexToF64(0x3f400144, 0xf0000000)] }, // ~0.0
+ { input: kValue.f32.negative.pi.half, expected: kAny },
+ { input: 0, expected: [hexToF64(0xbf400200, 0xb0000000), hexToF64(0x3f400200, 0xb0000000)] }, // ~0.0
+ { input: kValue.f32.positive.pi.half, expected: kAny },
+ { input: kValue.f32.positive.pi.whole, expected: [hexToF64(0xbf400144, 0xf0000000), hexToF64(0x3f4002bc, 0x90000000)] }, // ~0.0
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = tanInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `tanInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('tanhInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.negative.min, expected: kAny },
+ { input: -1, expected: [hexToF64(0xbfe85efd, 0x10000000), hexToF64(0xbfe85ef8, 0x90000000)] }, // ~-0.7615...
+ { input: 0, expected: [hexToF64(0xbe8c0000, 0xb0000000), hexToF64(0x3e8c0000, 0xb0000000)] }, // ~0
+ { input: 1, expected: [hexToF64(0x3fe85ef8, 0x90000000), hexToF64(0x3fe85efd, 0x10000000)] }, // ~0.7615...
+ { input: kValue.f32.positive.max, expected: kAny },
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = tanhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `tanhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('truncInterval')
+ .paramsSubcasesOnly<PointToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: 0, expected: [0] },
+ { input: 0.1, expected: [0] },
+ { input: 0.9, expected: [0] },
+ { input: 1.0, expected: [1] },
+ { input: 1.1, expected: [1] },
+ { input: 1.9, expected: [1] },
+ { input: -0.1, expected: [0] },
+ { input: -0.9, expected: [0] },
+ { input: -1.0, expected: [-1] },
+ { input: -1.1, expected: [-1] },
+ { input: -1.9, expected: [-1] },
+
+ // Edge cases
+ { input: kValue.f32.infinity.positive, expected: kAny },
+ { input: kValue.f32.infinity.negative, expected: kAny },
+ { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { input: kValue.f32.positive.min, expected: [0] },
+ { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { input: kValue.f32.negative.max, expected: [0] },
+
+ // 32-bit subnormals
+ { input: kValue.f32.subnormal.positive.max, expected: [0] },
+ { input: kValue.f32.subnormal.positive.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.min, expected: [0] },
+ { input: kValue.f32.subnormal.negative.max, expected: [0] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = truncInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `truncInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface BinaryToIntervalCase {
+ // input is a pair of independent values, not an range, so should not be
+ // converted to a F32Interval.
+ input: [number, number];
+ expected: IntervalBounds;
+}
+
+g.test('additionInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [1, 0], expected: [1] },
+ { input: [0, 1], expected: [1] },
+ { input: [-1, 0], expected: [-1] },
+ { input: [0, -1], expected: [-1] },
+ { input: [1, 1], expected: [2] },
+ { input: [1, -1], expected: [0] },
+ { input: [-1, 1], expected: [0] },
+ { input: [-1, -1], expected: [-2] },
+
+ // 64-bit normals
+ { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [0, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3e4ccccd)), hexToF32(0x3e4ccccd)] }, // ~0.2
+ { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0
+ { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0
+ { input: [-0.1, -0.1], expected: [hexToF32(0xbe4ccccd), plusOneULP(hexToF32(0xbe4ccccd))] }, // ~-0.2
+
+ // 32-bit subnormals
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [0, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: [kValue.f32.subnormal.negative.min, 0] },
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny},
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = additionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `additionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Note: atan2's parameters are labelled (y, x) instead of (x, y)
+g.test('atan2Interval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+
+ // The positive x & y quadrant is tested in more detail, and the other quadrants are spot checked that values are
+ // pointing in the right direction.
+
+ // Some of the intervals appear slightly asymmetric, i.e. [π/4 - 4097 * ULP(π/4), π/4 + 4096 * ULP(π/4)], this is
+ // because π/4 is not precisely expressible as a f32, so the higher precision value can be rounded up or down when
+ // converting to f32. Thus one option will be 1 ULP off of the constant value being used.
+
+ // positive y, positive x
+ { input: [1, hexToF32(0x3fddb3d7)], expected: [minusNULP(kValue.f32.positive.pi.sixth, 4097), plusNULP(kValue.f32.positive.pi.sixth, 4096)] }, // x = √3
+ { input: [1, 1], expected: [minusNULP(kValue.f32.positive.pi.quarter, 4097), plusNULP(kValue.f32.positive.pi.quarter, 4096)] },
+ // { input: [hexToF32(0x3fddb3d7), 1], expected: [hexToF64(0x3ff0bf52, 0x00000000), hexToF64(0x3ff0c352, 0x60000000)] }, // y = √3
+ { input: [Number.POSITIVE_INFINITY, 1], expected: kAny },
+
+ // positive y, negative x
+ { input: [1, -1], expected: [minusNULP(kValue.f32.positive.pi.three_quarters, 4096), plusNULP(kValue.f32.positive.pi.three_quarters, 4097)] },
+ { input: [Number.POSITIVE_INFINITY, -1], expected: kAny },
+
+ // negative y, negative x
+ { input: [-1, -1], expected: [minusNULP(kValue.f32.negative.pi.three_quarters, 4097), plusNULP(kValue.f32.negative.pi.three_quarters, 4096)] },
+ { input: [Number.NEGATIVE_INFINITY, -1], expected: kAny },
+
+ // negative y, positive x
+ { input: [-1, 1], expected: [minusNULP(kValue.f32.negative.pi.quarter, 4096), plusNULP(kValue.f32.negative.pi.quarter, 4097)] },
+ { input: [Number.NEGATIVE_INFINITY, 1], expected: kAny },
+
+ // Discontinuity @ origin (0,0)
+ { input: [0, 0], expected: kAny },
+ { input: [0, kValue.f32.subnormal.positive.max], expected: kAny },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: kAny },
+ { input: [0, kValue.f32.positive.min], expected: kAny },
+ { input: [0, kValue.f32.negative.max], expected: kAny },
+ { input: [0, kValue.f32.positive.max], expected: kAny },
+ { input: [0, kValue.f32.negative.min], expected: kAny },
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [0, 1], expected: kAny },
+ { input: [kValue.f32.subnormal.positive.max, 1], expected: kAny },
+ { input: [kValue.f32.subnormal.negative.min, 1], expected: kAny },
+
+ // When atan(y/x) ~ 0, test that ULP applied to result of atan2, not the intermediate atan(y/x) value
+ {input: [hexToF32(0x80800000), hexToF32(0xbf800000)], expected: [minusNULP(kValue.f32.negative.pi.whole, 4096), plusNULP(kValue.f32.negative.pi.whole, 4096)] },
+ {input: [hexToF32(0x00800000), hexToF32(0xbf800000)], expected: [minusNULP(kValue.f32.positive.pi.whole, 4096), plusNULP(kValue.f32.positive.pi.whole, 4096)] },
+
+ // Very large |x| values should cause kAny to be returned, due to the restrictions on division
+ { input: [1, kValue.f32.positive.max], expected: kAny },
+ { input: [1, kValue.f32.positive.nearest_max], expected: kAny },
+ { input: [1, kValue.f32.negative.min], expected: kAny },
+ { input: [1, kValue.f32.negative.nearest_min], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [y, x] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = atan2Interval(y, x);
+ t.expect(
+ objectEquals(expected, got),
+ `atan2Interval(${y}, ${x}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('distanceIntervalScalar')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult
+ // to express in a closed human readable form due to the inherited nature
+ // of the errors.
+ //
+ // distance(x, y), where x - y = 0 has an acceptance interval of kAny,
+ // because distance(x, y) = length(x - y), and length(0) = kAny
+ { input: [0, 0], expected: kAny },
+ { input: [1.0, 0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [1.0, 1.0], expected: kAny },
+ { input: [-0.0, -1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [0.0, -1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [-1.0, -1.0], expected: kAny },
+ { input: [0.1, 0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [0, 0.1], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [-0.1, 0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [0, -0.1], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [10.0, 0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+ { input: [0, 10.0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+ { input: [-10.0, 0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+ { input: [0, -10.0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10
+
+ // Subnormal Cases
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: kAny },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: kAny },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: kAny },
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: kAny },
+
+ // Edge cases
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.negative.min, 0], expected: kAny },
+ { input: [kValue.f32.negative.max, 0], expected: kAny },
+ { input: [kValue.f32.positive.min, 0], expected: kAny },
+ { input: [kValue.f32.positive.max, 0], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = distanceInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `distanceInterval(${t.params.input[0]}, ${t.params.input[1]}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('divisionInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 1], expected: [0] },
+ { input: [0, -1], expected: [0] },
+ { input: [1, 1], expected: [1] },
+ { input: [1, -1], expected: [-1] },
+ { input: [-1, 1], expected: [-1] },
+ { input: [-1, -1], expected: [1] },
+ { input: [4, 2], expected: [2] },
+ { input: [-4, 2], expected: [-2] },
+ { input: [4, -2], expected: [-2] },
+ { input: [-4, -2], expected: [2] },
+
+ // 64-bit normals
+ { input: [0, 0.1], expected: [0] },
+ { input: [0, -0.1], expected: [0] },
+ { input: [1, 0.1], expected: [minusOneULP(10), plusOneULP(10)] },
+ { input: [-1, 0.1], expected: [minusOneULP(-10), plusOneULP(-10)] },
+ { input: [1, -0.1], expected: [minusOneULP(-10), plusOneULP(-10)] },
+ { input: [-1, -0.1], expected: [minusOneULP(10), plusOneULP(10)] },
+
+ // Denominator out of range
+ { input: [1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [1, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ { input: [1, kValue.f32.positive.max], expected: kAny },
+ { input: [1, kValue.f32.negative.min], expected: kAny },
+ { input: [1, 0], expected: kAny },
+ { input: [1, kValue.f32.subnormal.positive.max], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const error = (n: number): number => {
+ return 2.5 * oneULP(n);
+ };
+
+ const [x, y] = t.params.input;
+ t.params.expected = applyError(t.params.expected, error);
+ const expected = toF32Interval(t.params.expected);
+
+ const got = divisionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('ldexpInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [0, 1], expected: [0] },
+ { input: [0, -1], expected: [0] },
+ { input: [1, 1], expected: [2] },
+ { input: [1, -1], expected: [0.5] },
+ { input: [-1, 1], expected: [-2] },
+ { input: [-1, -1], expected: [-0.5] },
+
+ // 64-bit normals
+ { input: [0, 0.1], expected: [0] },
+ { input: [0, -0.1], expected: [0] },
+ { input: [1.0000000001, 1], expected: [2, plusNULP(2, 2)] }, // ~2, additional ULP error due to first param not being f32 precise
+ { input: [-1.0000000001, 1], expected: [minusNULP(-2, 2), -2] }, // ~-2, additional ULP error due to first param not being f32 precise
+
+ // Edge Cases
+ { input: [1.9999998807907104, 127], expected: [kValue.f32.positive.max] },
+ { input: [1, -126], expected: [kValue.f32.positive.min] },
+ { input: [0.9999998807907104, -126], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [1.1920928955078125e-07, -126], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [-1.1920928955078125e-07, -126], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [-0.9999998807907104, -126], expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: [-1, -126], expected: [kValue.f32.negative.max] },
+ { input: [-1.9999998807907104, 127], expected: [kValue.f32.negative.min] },
+
+ // Out of Bounds
+ { input: [1, 128], expected: kAny },
+ { input: [-1, 128], expected: kAny },
+ { input: [100, 126], expected: kAny },
+ { input: [-100, 126], expected: kAny },
+ { input: [kValue.f32.positive.max, kValue.i32.positive.max], expected: kAny },
+ { input: [kValue.f32.negative.min, kValue.i32.positive.max], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = ldexpInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('maxInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [1, 0], expected: [1] },
+ { input: [0, 1], expected: [1] },
+ { input: [-1, 0], expected: [0] },
+ { input: [0, -1], expected: [0] },
+ { input: [1, 1], expected: [1] },
+ { input: [1, -1], expected: [1] },
+ { input: [-1, 1], expected: [1] },
+ { input: [-1, -1], expected: [-1] },
+
+ // 64-bit normals
+ { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [-0.1, 0], expected: [0] },
+ { input: [0, -0.1], expected: [0] },
+ { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [-0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+
+ // 32-bit normals
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.negative.max], expected: [0] },
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: [0] },
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = maxInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `maxInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('minInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [1, 0], expected: [0] },
+ { input: [0, 1], expected: [0] },
+ { input: [-1, 0], expected: [-1] },
+ { input: [0, -1], expected: [-1] },
+ { input: [1, 1], expected: [1] },
+ { input: [1, -1], expected: [-1] },
+ { input: [-1, 1], expected: [-1] },
+ { input: [-1, -1], expected: [-1] },
+
+ // 64-bit normals
+ { input: [0.1, 0], expected: [0] },
+ { input: [0, 0.1], expected: [0] },
+ { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [0, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [-0.1, 0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [-0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+
+ // 32-bit normals
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.max], expected: [0] },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.min], expected: [0] },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [0, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: [kValue.f32.subnormal.negative.min, 0] },
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = minInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `minInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('multiplicationInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [1, 0], expected: [0] },
+ { input: [0, 1], expected: [0] },
+ { input: [-1, 0], expected: [0] },
+ { input: [0, -1], expected: [0] },
+ { input: [1, 1], expected: [1] },
+ { input: [1, -1], expected: [-1] },
+ { input: [-1, 1], expected: [-1] },
+ { input: [-1, -1], expected: [1] },
+ { input: [2, 1], expected: [2] },
+ { input: [1, -2], expected: [-2] },
+ { input: [-2, 1], expected: [-2] },
+ { input: [-2, -1], expected: [2] },
+ { input: [2, 2], expected: [4] },
+ { input: [2, -2], expected: [-4] },
+ { input: [-2, 2], expected: [-4] },
+ { input: [-2, -2], expected: [4] },
+
+ // 64-bit normals
+ { input: [0.1, 0], expected: [0] },
+ { input: [0, 0.1], expected: [0] },
+ { input: [-0.1, 0], expected: [0] },
+ { input: [0, -0.1], expected: [0] },
+ { input: [0.1, 0.1], expected: [minusNULP(hexToF32(0x3c23d70a), 2), plusOneULP(hexToF32(0x3c23d70a))] }, // ~0.01
+ { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0xbc23d70a)), plusNULP(hexToF32(0xbc23d70a), 2)] }, // ~-0.01
+ { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0xbc23d70a)), plusNULP(hexToF32(0xbc23d70a), 2)] }, // ~-0.01
+ { input: [-0.1, -0.1], expected: [minusNULP(hexToF32(0x3c23d70a), 2), plusOneULP(hexToF32(0x3c23d70a))] }, // ~0.01
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [-1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [1, kValue.f32.infinity.negative], expected: kAny },
+ { input: [-1, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+
+ // Edge of f32
+ { input: [kValue.f32.positive.max, kValue.f32.positive.max], expected: kAny },
+ { input: [kValue.f32.negative.min, kValue.f32.negative.min], expected: kAny },
+ { input: [kValue.f32.positive.max, kValue.f32.negative.min], expected: kAny },
+ { input: [kValue.f32.negative.min, kValue.f32.positive.max], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = multiplicationInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `multiplicationInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('remainderInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 1], expected: [0, 0] },
+ { input: [0, -1], expected: [0, 0] },
+ { input: [1, 1], expected: [0, 1] },
+ { input: [1, -1], expected: [0, 1] },
+ { input: [-1, 1], expected: [-1, 0] },
+ { input: [-1, -1], expected: [-1, 0] },
+ { input: [4, 2], expected: [0, 2] },
+ { input: [-4, 2], expected: [-2, 0] },
+ { input: [4, -2], expected: [0, 2] },
+ { input: [-4, -2], expected: [-2, 0] },
+ { input: [2, 4], expected: [2, 2] },
+ { input: [-2, 4], expected: [-2, -2] },
+ { input: [2, -4], expected: [2, 2] },
+ { input: [-2, -4], expected: [-2, -2] },
+
+ // 64-bit normals
+ { input: [0, 0.1], expected: [0, 0] },
+ { input: [0, -0.1], expected: [0, 0] },
+ { input: [1, 0.1], expected: [hexToF32(0xb4000000), hexToF32(0x3dccccd8)] }, // ~[0, 0.1]
+ { input: [-1, 0.1], expected: [hexToF32(0xbdccccd8), hexToF32(0x34000000)] }, // ~[-0.1, 0]
+ { input: [1, -0.1], expected: [hexToF32(0xb4000000), hexToF32(0x3dccccd8)] }, // ~[0, 0.1]
+ { input: [-1, -0.1], expected: [hexToF32(0xbdccccd8), hexToF32(0x34000000)] }, // ~[-0.1, 0]
+
+ // Denominator out of range
+ { input: [1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [1, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ { input: [1, kValue.f32.positive.max], expected: kAny },
+ { input: [1, kValue.f32.negative.min], expected: kAny },
+ { input: [1, 0], expected: kAny },
+ { input: [1, kValue.f32.subnormal.positive.max], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = remainderInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `remainderInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('powInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: [-1, 0], expected: kAny },
+ { input: [0, 0], expected: kAny },
+ { input: [1, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1
+ { input: [2, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1
+ { input: [kValue.f32.positive.max, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1
+ { input: [0, 1], expected: kAny },
+ { input: [1, 1], expected: [hexToF64(0x3feffffe, 0xdffffe00), hexToF64(0x3ff00000, 0xc0000200)] }, // ~1
+ { input: [1, 100], expected: [hexToF64(0x3fefffba, 0x3fff3800), hexToF64(0x3ff00023, 0x2000c800)] }, // ~1
+ { input: [1, kValue.f32.positive.max], expected: kAny },
+ { input: [2, 1], expected: [hexToF64(0x3ffffffe, 0xa0000200), hexToF64(0x40000001, 0x00000200)] }, // ~2
+ { input: [2, 2], expected: [hexToF64(0x400ffffd, 0xa0000400), hexToF64(0x40100001, 0xa0000400)] }, // ~4
+ { input: [10, 10], expected: [hexToF64(0x4202a04f, 0x51f77000), hexToF64(0x4202a070, 0xee08e000)] }, // ~10000000000
+ { input: [10, 1], expected: [hexToF64(0x4023fffe, 0x0b658b00), hexToF64(0x40240002, 0x149a7c00)] }, // ~10
+ { input: [kValue.f32.positive.max, 1], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = powInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `powInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('stepInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [1] },
+ { input: [1, 1], expected: [1] },
+ { input: [0, 1], expected: [1] },
+ { input: [1, 0], expected: [0] },
+ { input: [-1, -1], expected: [1] },
+ { input: [0, -1], expected: [0] },
+ { input: [-1, 0], expected: [1] },
+ { input: [-1, 1], expected: [1] },
+ { input: [1, -1], expected: [0] },
+
+ // 64-bit normals
+ { input: [0.1, 0.1], expected: [0, 1] },
+ { input: [0, 0.1], expected: [1] },
+ { input: [0.1, 0], expected: [0] },
+ { input: [0.1, 1], expected: [1] },
+ { input: [1, 0.1], expected: [0] },
+ { input: [-0.1, -0.1], expected: [0, 1] },
+ { input: [0, -0.1], expected: [0] },
+ { input: [-0.1, 0], expected: [1] },
+ { input: [-0.1, -1], expected: [0] },
+ { input: [-1, -0.1], expected: [1] },
+
+ // Subnormals
+ { input: [0, kValue.f32.subnormal.positive.max], expected: [1] },
+ { input: [0, kValue.f32.subnormal.positive.min], expected: [1] },
+ { input: [0, kValue.f32.subnormal.negative.max], expected: [0, 1] },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: [0, 1] },
+ { input: [1, kValue.f32.subnormal.positive.max], expected: [0] },
+ { input: [1, kValue.f32.subnormal.positive.min], expected: [0] },
+ { input: [1, kValue.f32.subnormal.negative.max], expected: [0] },
+ { input: [1, kValue.f32.subnormal.negative.min], expected: [0] },
+ { input: [-1, kValue.f32.subnormal.positive.max], expected: [1] },
+ { input: [-1, kValue.f32.subnormal.positive.min], expected: [1] },
+ { input: [-1, kValue.f32.subnormal.negative.max], expected: [1] },
+ { input: [-1, kValue.f32.subnormal.negative.min], expected: [1] },
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, 1] },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, 1] },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: [1] },
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: [1] },
+ { input: [kValue.f32.subnormal.positive.max, 1], expected: [1] },
+ { input: [kValue.f32.subnormal.positive.min, 1], expected: [1] },
+ { input: [kValue.f32.subnormal.negative.max, 1], expected: [1] },
+ { input: [kValue.f32.subnormal.negative.min, 1], expected: [1] },
+ { input: [kValue.f32.subnormal.positive.max, -1], expected: [0] },
+ { input: [kValue.f32.subnormal.positive.min, -1], expected: [0] },
+ { input: [kValue.f32.subnormal.negative.max, -1], expected: [0] },
+ { input: [kValue.f32.subnormal.negative.min, -1], expected: [0] },
+ { input: [kValue.f32.subnormal.negative.min, kValue.f32.subnormal.positive.max], expected: [1] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min], expected: [0, 1] },
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [edge, x] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = stepInterval(edge, x);
+ t.expect(
+ objectEquals(expected, got),
+ `stepInterval(${edge}, ${x}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('subtractionInterval')
+ .paramsSubcasesOnly<BinaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // 32-bit normals
+ { input: [0, 0], expected: [0] },
+ { input: [1, 0], expected: [1] },
+ { input: [0, 1], expected: [-1] },
+ { input: [-1, 0], expected: [-1] },
+ { input: [0, -1], expected: [1] },
+ { input: [1, 1], expected: [0] },
+ { input: [1, -1], expected: [2] },
+ { input: [-1, 1], expected: [-2] },
+ { input: [-1, -1], expected: [0] },
+
+ // 64-bit normals
+ { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0, 0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1
+ { input: [0, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1
+ { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0.0
+ { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3e4ccccd)), hexToF32(0x3e4ccccd)] }, // ~0.2
+ { input: [-0.1, 0.1], expected: [hexToF32(0xbe4ccccd), plusOneULP(hexToF32(0xbe4ccccd))] }, // ~-0.2
+ { input: [-0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0
+
+ // // 32-bit normals
+ { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [0, kValue.f32.subnormal.positive.max], expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, kValue.f32.subnormal.positive.min], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [0, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] },
+ { input: [0, kValue.f32.subnormal.negative.min], expected: [0, kValue.f32.subnormal.positive.max] },
+
+ // Infinities
+ { input: [0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 0], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = subtractionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `subtractionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface TernaryToIntervalCase {
+ input: [number, number, number];
+ expected: IntervalBounds;
+}
+
+g.test('clampMedianInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Normals
+ { input: [0, 0, 0], expected: [0] },
+ { input: [1, 0, 0], expected: [0] },
+ { input: [0, 1, 0], expected: [0] },
+ { input: [0, 0, 1], expected: [0] },
+ { input: [1, 0, 1], expected: [1] },
+ { input: [1, 1, 0], expected: [1] },
+ { input: [0, 1, 1], expected: [1] },
+ { input: [1, 1, 1], expected: [1] },
+ { input: [1, 10, 100], expected: [10] },
+ { input: [10, 1, 100], expected: [10] },
+ { input: [100, 1, 10], expected: [10] },
+ { input: [-10, 1, 100], expected: [1] },
+ { input: [10, 1, -100], expected: [1] },
+ { input: [-10, 1, -100], expected: [-10] },
+ { input: [-10, -10, -10], expected: [-10] },
+
+ // Subnormals
+ { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] },
+ { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0] },
+ { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: [kValue.f32.positive.max] },
+
+ // Infinities
+ { input: [0, 1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = clampMedianInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `clampMedianInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('clampMinMaxInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Normals
+ { input: [0, 0, 0], expected: [0] },
+ { input: [1, 0, 0], expected: [0] },
+ { input: [0, 1, 0], expected: [0] },
+ { input: [0, 0, 1], expected: [0] },
+ { input: [1, 0, 1], expected: [1] },
+ { input: [1, 1, 0], expected: [0] },
+ { input: [0, 1, 1], expected: [1] },
+ { input: [1, 1, 1], expected: [1] },
+ { input: [1, 10, 100], expected: [10] },
+ { input: [10, 1, 100], expected: [10] },
+ { input: [100, 1, 10], expected: [10] },
+ { input: [-10, 1, 100], expected: [1] },
+ { input: [10, 1, -100], expected: [-100] },
+ { input: [-10, 1, -100], expected: [-100] },
+ { input: [-10, -10, -10], expected: [-10] },
+
+ // Subnormals
+ { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] },
+ { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0] },
+ { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] },
+ { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] },
+
+ // Infinities
+ { input: [0, 1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = clampMinMaxInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `clampMinMaxInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('fmaInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Normals
+ { input: [0, 0, 0], expected: [0] },
+ { input: [1, 0, 0], expected: [0] },
+ { input: [0, 1, 0], expected: [0] },
+ { input: [0, 0, 1], expected: [1] },
+ { input: [1, 0, 1], expected: [1] },
+ { input: [1, 1, 0], expected: [1] },
+ { input: [0, 1, 1], expected: [1] },
+ { input: [1, 1, 1], expected: [2] },
+ { input: [1, 10, 100], expected: [110] },
+ { input: [10, 1, 100], expected: [110] },
+ { input: [100, 1, 10], expected: [110] },
+ { input: [-10, 1, 100], expected: [90] },
+ { input: [10, 1, -100], expected: [-90] },
+ { input: [-10, 1, -100], expected: [-110] },
+ { input: [-10, -10, -10], expected: [90] },
+
+ // Subnormals
+ { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] },
+ { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] },
+ { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.positive.min] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [hexToF32(0x80000002), 0] },
+
+ // Infinities
+ { input: [0, 1, kValue.f32.infinity.positive], expected: kAny },
+ { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny },
+ { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = fmaInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `fmaInterval(${t.params.input.join(',')}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('mixImpreciseInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, -1.0], expected: [-1.0] },
+ { input: [0.0, 1.0, 0.0], expected: [0.0] },
+ { input: [0.0, 1.0, 0.1], expected: [hexToF64(0x3fb99999,0x80000000), hexToF64(0x3fb99999,0xa0000000)] }, // ~0.1
+ { input: [0.0, 1.0, 0.5], expected: [0.5] },
+ { input: [0.0, 1.0, 0.9], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9
+ { input: [0.0, 1.0, 1.0], expected: [1.0] },
+ { input: [0.0, 1.0, 2.0], expected: [2.0] },
+
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, -1.0], expected: [2.0] },
+ { input: [1.0, 0.0, 0.0], expected: [1.0] },
+ { input: [1.0, 0.0, 0.1], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9
+ { input: [1.0, 0.0, 0.5], expected: [0.5] },
+ { input: [1.0, 0.0, 0.9], expected: [hexToF64(0x3fb99999,0x00000000), hexToF64(0x3fb9999a,0x00000000)] }, // ~0.1
+ { input: [1.0, 0.0, 1.0], expected: [0.0] },
+ { input: [1.0, 0.0, 2.0], expected: [-1.0] },
+
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, -1.0], expected: [-10.0] },
+ { input: [0.0, 10.0, 0.0], expected: [0.0] },
+ { input: [0.0, 10.0, 0.1], expected: [hexToF64(0x3fefffff,0xe0000000), hexToF64(0x3ff00000,0x20000000)] }, // ~1
+ { input: [0.0, 10.0, 0.5], expected: [5.0] },
+ { input: [0.0, 10.0, 0.9], expected: [hexToF64(0x4021ffff,0xe0000000), hexToF64(0x40220000,0x20000000)] }, // ~9
+ { input: [0.0, 10.0, 1.0], expected: [10.0] },
+ { input: [0.0, 10.0, 2.0], expected: [20.0] },
+
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, -1.0], expected: [-6.0] },
+ { input: [2.0, 10.0, 0.0], expected: [2.0] },
+ { input: [2.0, 10.0, 0.1], expected: [hexToF64(0x40066666,0x60000000), hexToF64(0x40066666,0x80000000)] }, // ~2.8
+ { input: [2.0, 10.0, 0.5], expected: [6.0] },
+ { input: [2.0, 10.0, 0.9], expected: [hexToF64(0x40226666,0x60000000), hexToF64(0x40226666,0x80000000)] }, // ~9.2
+ { input: [2.0, 10.0, 1.0], expected: [10.0] },
+ { input: [2.0, 10.0, 2.0], expected: [18.0] },
+
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, -2.0], expected: [-5.0] },
+ { input: [-1.0, 1.0, 0.0], expected: [-1.0] },
+ { input: [-1.0, 1.0, 0.1], expected: [hexToF64(0xbfe99999,0xa0000000), hexToF64(0xbfe99999,0x80000000)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.5], expected: [0.0] },
+ { input: [-1.0, 1.0, 0.9], expected: [hexToF64(0x3fe99999,0x80000000), hexToF64(0x3fe99999,0xc0000000)] }, // ~0.8
+ { input: [-1.0, 1.0, 1.0], expected: [1.0] },
+ { input: [-1.0, 1.0, 2.0], expected: [3.0] },
+
+ // Infinities
+ { input: [0.0, kValue.f32.infinity.positive, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0.0, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 1.0, 0.5], expected: kAny },
+ { input: [1.0, kValue.f32.infinity.negative, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative, 0.5], expected: kAny },
+ { input: [0.0, 1.0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [1.0, 0.0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [0.0, 1.0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [1.0, 0.0, kValue.f32.infinity.positive], expected: kAny },
+
+ // Showing how precise and imprecise versions diff
+ { input: [kValue.f32.negative.min, 10.0, 1.0], expected: [0.0]},
+ ]
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = mixImpreciseInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `mixImpreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('mixPreciseInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, -1.0], expected: [-1.0] },
+ { input: [0.0, 1.0, 0.0], expected: [0.0] },
+ { input: [0.0, 1.0, 0.1], expected: [hexToF64(0x3fb99999,0x80000000), hexToF64(0x3fb99999,0xa0000000)] }, // ~0.1
+ { input: [0.0, 1.0, 0.5], expected: [0.5] },
+ { input: [0.0, 1.0, 0.9], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9
+ { input: [0.0, 1.0, 1.0], expected: [1.0] },
+ { input: [0.0, 1.0, 2.0], expected: [2.0] },
+
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, -1.0], expected: [2.0] },
+ { input: [1.0, 0.0, 0.0], expected: [1.0] },
+ { input: [1.0, 0.0, 0.1], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9
+ { input: [1.0, 0.0, 0.5], expected: [0.5] },
+ { input: [1.0, 0.0, 0.9], expected: [hexToF64(0x3fb99999,0x00000000), hexToF64(0x3fb9999a,0x00000000)] }, // ~0.1
+ { input: [1.0, 0.0, 1.0], expected: [0.0] },
+ { input: [1.0, 0.0, 2.0], expected: [-1.0] },
+
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, -1.0], expected: [-10.0] },
+ { input: [0.0, 10.0, 0.0], expected: [0.0] },
+ { input: [0.0, 10.0, 0.1], expected: [hexToF64(0x3fefffff,0xe0000000), hexToF64(0x3ff00000,0x20000000)] }, // ~1
+ { input: [0.0, 10.0, 0.5], expected: [5.0] },
+ { input: [0.0, 10.0, 0.9], expected: [hexToF64(0x4021ffff,0xe0000000), hexToF64(0x40220000,0x20000000)] }, // ~9
+ { input: [0.0, 10.0, 1.0], expected: [10.0] },
+ { input: [0.0, 10.0, 2.0], expected: [20.0] },
+
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, -1.0], expected: [-6.0] },
+ { input: [2.0, 10.0, 0.0], expected: [2.0] },
+ { input: [2.0, 10.0, 0.1], expected: [hexToF64(0x40066666,0x40000000), hexToF64(0x40066666,0x80000000)] }, // ~2.8
+ { input: [2.0, 10.0, 0.5], expected: [6.0] },
+ { input: [2.0, 10.0, 0.9], expected: [hexToF64(0x40226666,0x40000000), hexToF64(0x40226666,0xa0000000)] }, // ~9.2
+ { input: [2.0, 10.0, 1.0], expected: [10.0] },
+ { input: [2.0, 10.0, 2.0], expected: [18.0] },
+
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, -2.0], expected: [-5.0] },
+ { input: [-1.0, 1.0, 0.0], expected: [-1.0] },
+ { input: [-1.0, 1.0, 0.1], expected: [hexToF64(0xbfe99999,0xc0000000), hexToF64(0xbfe99999,0x80000000)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.5], expected: [0.0] },
+ { input: [-1.0, 1.0, 0.9], expected: [hexToF64(0x3fe99999,0x80000000), hexToF64(0x3fe99999,0xc0000000)] }, // ~0.8
+ { input: [-1.0, 1.0, 1.0], expected: [1.0] },
+ { input: [-1.0, 1.0, 2.0], expected: [3.0] },
+
+ // Infinities
+ { input: [0.0, kValue.f32.infinity.positive, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.positive, 0.0, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.negative, 1.0, 0.5], expected: kAny },
+ { input: [1.0, kValue.f32.infinity.negative, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, 0.5], expected: kAny },
+ { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative, 0.5], expected: kAny },
+ { input: [0.0, 1.0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [1.0, 0.0, kValue.f32.infinity.negative], expected: kAny },
+ { input: [0.0, 1.0, kValue.f32.infinity.positive], expected: kAny },
+ { input: [1.0, 0.0, kValue.f32.infinity.positive], expected: kAny },
+
+ // Showing how precise and imprecise versions diff
+ { input: [kValue.f32.negative.min, 10.0, 1.0], expected: [10.0]},
+ ]
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = mixPreciseInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `mixPreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('smoothStepInterval')
+ .paramsSubcasesOnly<TernaryToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ // Normals
+ { input: [0, 1, 0], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, 1, 1], expected: [hexToF32(0x3f7ffffa), hexToF32(0x3f800003)] }, // ~1
+ { input: [0, 1, 10], expected: [1] },
+ { input: [0, 1, -10], expected: [0] },
+ { input: [0, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [0, 2, 0.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625...
+ { input: [2, 0, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [2, 0, 1.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625...
+ { input: [0, 100, 50], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [0, 100, 25], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625...
+ { input: [0, -2, -1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [0, -2, -0.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625...
+
+ // Subnormals
+ { input: [0, 2, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, 2, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, 2, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [0, 2, kValue.f32.subnormal.negative.min], expected: [0, kValue.f32.subnormal.positive.min] },
+ { input: [kValue.f32.subnormal.positive.max, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.subnormal.positive.min, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.subnormal.negative.max, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.subnormal.negative.min, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5
+ { input: [0, kValue.f32.subnormal.positive.max, 1], expected: kAny },
+ { input: [0, kValue.f32.subnormal.positive.min, 1], expected: kAny },
+ { input: [0, kValue.f32.subnormal.negative.max, 1], expected: kAny },
+ { input: [0, kValue.f32.subnormal.negative.min, 1], expected: kAny },
+
+ // Infinities
+ { input: [0, 2, Number.POSITIVE_INFINITY], expected: kAny },
+ { input: [0, 2, Number.NEGATIVE_INFINITY], expected: kAny },
+ { input: [Number.POSITIVE_INFINITY, 2, 1], expected: kAny },
+ { input: [Number.NEGATIVE_INFINITY, 2, 1], expected: kAny },
+ { input: [0, Number.POSITIVE_INFINITY, 1], expected: kAny },
+ { input: [0, Number.NEGATIVE_INFINITY, 1], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const [low, high, x] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = smoothStepInterval(low, high, x);
+ t.expect(
+ objectEquals(expected, got),
+ `smoothStepInterval(${low}, ${high}, ${x}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface PointToVectorCase {
+ input: number;
+ expected: IntervalBounds[];
+}
+
+// Scope for unpack* tests so that they can have constants for magic numbers
+// that don't pollute the global namespace or have unwieldy long names.
+{
+ const kZeroBounds: IntervalBounds = [hexToF32(0x81200000), hexToF32(0x01200000)];
+ const kOneBoundsSnorm: IntervalBounds = [
+ hexToF64(0x3fefffff, 0xa0000000),
+ hexToF64(0x3ff00000, 0x40000000),
+ ];
+ const kOneBoundsUnorm: IntervalBounds = [
+ hexToF64(0x3fefffff, 0xb0000000),
+ hexToF64(0x3ff00000, 0x28000000),
+ ];
+ const kNegOneBoundsSnorm: IntervalBounds = [
+ hexToF64(0xbff00000, 0x00000000),
+ hexToF64(0xbfefffff, 0xa0000000),
+ ];
+
+ const kHalfBounds2x16snorm: IntervalBounds = [
+ hexToF64(0x3fe0001f, 0xa0000000),
+ hexToF64(0x3fe00020, 0x80000000),
+ ]; // ~0.5..., due to lack of precision in i16
+ const kNegHalfBounds2x16snorm: IntervalBounds = [
+ hexToF64(0xbfdfffc0, 0x60000000),
+ hexToF64(0xbfdfffbf, 0x80000000),
+ ]; // ~-0.5..., due to lack of precision in i16
+
+ g.test('unpack2x16snormInterval')
+ .paramsSubcasesOnly<PointToVectorCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] },
+ { input: 0x00007fff, expected: [kOneBoundsSnorm, kZeroBounds] },
+ { input: 0x7fff0000, expected: [kZeroBounds, kOneBoundsSnorm] },
+ { input: 0x7fff7fff, expected: [kOneBoundsSnorm, kOneBoundsSnorm] },
+ { input: 0x80018001, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm] },
+ { input: 0x40004000, expected: [kHalfBounds2x16snorm, kHalfBounds2x16snorm] },
+ { input: 0xc001c001, expected: [kNegHalfBounds2x16snorm, kNegHalfBounds2x16snorm] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Vector(t.params.expected);
+
+ const got = unpack2x16snormInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16snormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+ g.test('unpack2x16floatInterval')
+ .paramsSubcasesOnly<PointToVectorCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ // f16 normals
+ { input: 0x00000000, expected: [[0], [0]] },
+ { input: 0x80000000, expected: [[0], [0]] },
+ { input: 0x00008000, expected: [[0], [0]] },
+ { input: 0x80008000, expected: [[0], [0]] },
+ { input: 0x00003c00, expected: [[1], [0]] },
+ { input: 0x3c000000, expected: [[0], [1]] },
+ { input: 0x3c003c00, expected: [[1], [1]] },
+ { input: 0xbc00bc00, expected: [[-1], [-1]] },
+ { input: 0x49004900, expected: [[10], [10]] },
+ { input: 0xc900c900, expected: [[-10], [-10]] },
+
+ // f16 subnormals
+ { input: 0x000003ff, expected: [[0, kValue.f16.subnormal.positive.max], [0]] },
+ { input: 0x000083ff, expected: [[kValue.f16.subnormal.negative.min, 0], [0]] },
+
+ // f16 out of bounds
+ { input: 0x7c000000, expected: [kAny, kAny] },
+ { input: 0xffff0000, expected: [kAny, kAny] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Vector(t.params.expected);
+
+ const got = unpack2x16floatInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16floatInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+ const kHalfBounds2x16unorm: IntervalBounds = [
+ hexToF64(0x3fe0000f, 0xb0000000),
+ hexToF64(0x3fe00010, 0x70000000),
+ ]; // ~0.5..., due to lack of precision in u16
+
+ g.test('unpack2x16unormInterval')
+ .paramsSubcasesOnly<PointToVectorCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] },
+ { input: 0x0000ffff, expected: [kOneBoundsUnorm, kZeroBounds] },
+ { input: 0xffff0000, expected: [kZeroBounds, kOneBoundsUnorm] },
+ { input: 0xffffffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm] },
+ { input: 0x80008000, expected: [kHalfBounds2x16unorm, kHalfBounds2x16unorm] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Vector(t.params.expected);
+
+ const got = unpack2x16unormInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16unormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+ const kHalfBounds4x8snorm: IntervalBounds = [
+ hexToF64(0x3fe02040, 0x20000000),
+ hexToF64(0x3fe02041, 0x00000000),
+ ]; // ~0.50196..., due to lack of precision in i8
+ const kNegHalfBounds4x8snorm: IntervalBounds = [
+ hexToF64(0xbfdfbf7f, 0x60000000),
+ hexToF64(0xbfdfbf7e, 0x80000000),
+ ]; // ~-0.49606..., due to lack of precision in i8
+
+ g.test('unpack4x8snormInterval')
+ .paramsSubcasesOnly<PointToVectorCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x0000007f, expected: [kOneBoundsSnorm, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x00007f00, expected: [kZeroBounds, kOneBoundsSnorm, kZeroBounds, kZeroBounds] },
+ { input: 0x007f0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsSnorm, kZeroBounds] },
+ { input: 0x7f000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBoundsSnorm] },
+ { input: 0x00007f7f, expected: [kOneBoundsSnorm, kOneBoundsSnorm, kZeroBounds, kZeroBounds] },
+ { input: 0x7f7f0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsSnorm, kOneBoundsSnorm] },
+ { input: 0x7f007f00, expected: [kZeroBounds, kOneBoundsSnorm, kZeroBounds, kOneBoundsSnorm] },
+ { input: 0x007f007f, expected: [kOneBoundsSnorm, kZeroBounds, kOneBoundsSnorm, kZeroBounds] },
+ { input: 0x7f7f7f7f, expected: [kOneBoundsSnorm, kOneBoundsSnorm, kOneBoundsSnorm, kOneBoundsSnorm] },
+ { input: 0x81818181, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm, kNegOneBoundsSnorm, kNegOneBoundsSnorm] },
+ { input: 0x40404040, expected: [kHalfBounds4x8snorm, kHalfBounds4x8snorm, kHalfBounds4x8snorm, kHalfBounds4x8snorm] },
+ { input: 0xc1c1c1c1, expected: [kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Vector(t.params.expected);
+
+ const got = unpack4x8snormInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `unpack4x8snormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+ const kHalfBounds4x8unorm: IntervalBounds = [
+ hexToF64(0x3fe0100f, 0xb0000000),
+ hexToF64(0x3fe01010, 0x70000000),
+ ]; // ~0.50196..., due to lack of precision in u8
+
+ g.test('unpack4x8unormInterval')
+ .paramsSubcasesOnly<PointToVectorCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x000000ff, expected: [kOneBoundsUnorm, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x0000ff00, expected: [kZeroBounds, kOneBoundsUnorm, kZeroBounds, kZeroBounds] },
+ { input: 0x00ff0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsUnorm, kZeroBounds] },
+ { input: 0xff000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBoundsUnorm] },
+ { input: 0x0000ffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm, kZeroBounds, kZeroBounds] },
+ { input: 0xffff0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsUnorm, kOneBoundsUnorm] },
+ { input: 0xff00ff00, expected: [kZeroBounds, kOneBoundsUnorm, kZeroBounds, kOneBoundsUnorm] },
+ { input: 0x00ff00ff, expected: [kOneBoundsUnorm, kZeroBounds, kOneBoundsUnorm, kZeroBounds] },
+ { input: 0xffffffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm, kOneBoundsUnorm, kOneBoundsUnorm] },
+ { input: 0x80808080, expected: [kHalfBounds4x8unorm, kHalfBounds4x8unorm, kHalfBounds4x8unorm, kHalfBounds4x8unorm] },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Vector(t.params.expected);
+
+ const got = unpack4x8unormInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `unpack4x8unormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+}
+
+interface VectorToIntervalCase {
+ input: number[];
+ expected: IntervalBounds;
+}
+
+g.test('lengthIntervalVector')
+ .paramsSubcasesOnly<VectorToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable
+ // form due to the inherited nature of the errors.
+ // vec2
+ {input: [1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [1.0, 1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2
+ {input: [-1.0, -1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2
+ {input: [-1.0, 1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2
+ {input: [0.1, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+
+ // vec3
+ {input: [1.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [1.0, 1.0, 1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ {input: [-1.0, -1.0, -1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ {input: [1.0, -1.0, -1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ {input: [0.1, 0.0, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+
+ // vec4
+ {input: [1.0, 0.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 1.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 0.0, 1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [0.0, 0.0, 0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ {input: [1.0, 1.0, 1.0, 1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ {input: [-1.0, -1.0, -1.0, -1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ {input: [-1.0, 1.0, -1.0, 1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ {input: [0.1, 0.0, 0.0, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], expected: kAny },
+ { input: [kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], expected: kAny },
+ { input: [kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], expected: kAny },
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = lengthInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `lengthInterval([${t.params.input}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorPairToIntervalCase {
+ input: [number[], number[]];
+ expected: IntervalBounds;
+}
+
+g.test('distanceIntervalVector')
+ .paramsSubcasesOnly<VectorPairToIntervalCase>(
+ // prettier-ignore
+ [
+ // Some of these are hard coded, since the error intervals are difficult
+ // to express in a closed human readable form due to the inherited nature
+ // of the errors.
+ //
+ // distance(x, y), where x - y = 0 has an acceptance interval of kAny,
+ // because distance(x, y) = length(x - y), and length(0) = kAny
+
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: kAny },
+ { input: [[1.0, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0], [1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[-1.0, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0], [-1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 1.0], [-1.0, 0.0]], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2
+ { input: [[0.1, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kAny },
+ { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[1.0, 1.0, 1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ { input: [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ { input: [[-1.0, -1.0, -1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ { input: [[0.0, 0.0, 0.0], [-1.0, -1.0, -1.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3
+ { input: [[0.1, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kAny },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1
+ { input: [[1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ { input: [[-1.0, 1.0, -1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, -1.0, 1.0, -1.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2
+ { input: [[0.1, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1
+ ]
+ )
+ .fn(t => {
+ const expected = toF32Interval(t.params.expected);
+
+ const got = distanceInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `distanceInterval([${t.params.input[0]}, ${t.params.input[1]}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('dotInterval')
+ .paramsSubcasesOnly<VectorPairToIntervalCase>(
+ // prettier-ignore
+ [
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 1.0], [0.0, 1.0]], expected: [1.0] },
+ { input: [[1.0, 1.0], [1.0, 1.0]], expected: [2.0] },
+ { input: [[-1.0, -1.0], [-1.0, -1.0]], expected: [2.0] },
+ { input: [[-1.0, 1.0], [1.0, -1.0]], expected: [-2.0] },
+ { input: [[0.1, 0.0], [1.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: [1.0] },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [3.0] },
+ { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: [3.0] },
+ { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [-1.0] },
+ { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [1.0] },
+ { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]], expected: [1.0] },
+ { input: [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], expected: [4.0] },
+ { input: [[-1.0, -1.0, -1.0, -1.0], [-1.0, -1.0, -1.0, -1.0]], expected: [4.0] },
+ { input: [[-1.0, 1.0, -1.0, 1.0], [1.0, -1.0, 1.0, -1.0]], expected: [-4.0] },
+ { input: [[0.1, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1
+
+ // Test that going out of bounds in the intermediate calculations is caught correctly.
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: kAny },
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: kAny },
+ { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: kAny },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: kAny },
+ { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kAny },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kAny },
+
+ // https://github.com/gpuweb/cts/issues/2155
+ { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [-13, 0] },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Interval(t.params.expected);
+
+ const got = dotInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `dotInterval([${x}], [${y}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorToVectorCase {
+ input: number[];
+ expected: IntervalBounds[];
+}
+
+g.test('normalizeInterval')
+ .paramsSubcasesOnly<VectorToVectorCase>(
+ // prettier-ignore
+ [
+ // vec2
+ {input: [1.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0]
+ {input: [0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~1.0]
+ {input: [-1.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0]
+ {input: [1.0, 1.0], expected: [[hexToF64(0x3fe6a09d, 0x50000000), hexToF64(0x3fe6a09f, 0x90000000)], [hexToF64(0x3fe6a09d, 0x50000000), hexToF64(0x3fe6a09f, 0x90000000)]] }, // [ ~1/√2, ~1/√2]
+
+ // vec3
+ {input: [1.0, 0.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0], expected: [[hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)], [hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)], [hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)]] }, // [ ~1/√3, ~1/√3, ~1/√3]
+
+ // vec4
+ {input: [1.0, 0.0, 0.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 0.0, 1.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0, 1.0], expected: [[hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)]] }, // [ ~1/√4, ~1/√4, ~1/√4]
+ ]
+ )
+ .fn(t => {
+ const x = t.params.input;
+ const expected = toF32Vector(t.params.expected);
+
+ const got = normalizeInterval(x);
+ t.expect(
+ objectEquals(expected, got),
+ `normalizeInterval([${x}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorPairToVectorCase {
+ input: [number[], number[]];
+ expected: IntervalBounds[];
+}
+
+g.test('crossInterval')
+ .paramsSubcasesOnly<VectorPairToVectorCase>(
+ // prettier-ignore
+ [
+ // parallel vectors, AXB == 0
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] },
+ { input: [[kValue.f32.subnormal.positive.max, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] },
+
+ // non-parallel vectors, AXB != 0
+ // f32 normals
+ { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [[2.0], [2.0], [0.0]] },
+ { input: [[1.0, 2, 3], [1.0, 5.0, 7.0]], expected: [[-1], [-4], [3]] },
+
+ // f64 normals
+ { input: [[0.1, -0.1, -0.1], [-0.1, 0.1, -0.1]],
+ expected: [[hexToF32(0x3ca3d708), hexToF32(0x3ca3d70b)], // ~0.02
+ [hexToF32(0x3ca3d708), hexToF32(0x3ca3d70b)], // ~0.02
+ [hexToF32(0xb1400000), hexToF32(0x31400000)]] }, // ~0
+
+ // f32 subnormals
+ { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, kValue.f32.subnormal.negative.min],
+ [kValue.f32.subnormal.negative.min, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max]],
+ expected: [[0.0, hexToF32(0x00000002)], // ~0
+ [0.0, hexToF32(0x00000002)], // ~0
+ [hexToF32(0x80000001), hexToF32(0x00000001)]] }, // ~0
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Vector(t.params.expected);
+
+ const got = crossInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `crossInterval([${x}], [${y}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('reflectInterval')
+ .paramsSubcasesOnly<VectorPairToVectorCase>(
+ // prettier-ignore
+ [
+ // vec2s
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: [[-1.0], [0.0]] },
+ { input: [[1.0, 0.0], [0.0, 1.0]], expected: [[1.0], [0.0]] },
+ { input: [[0.0, 1.0], [0.0, 1.0]], expected: [[0.0], [-1.0]] },
+ { input: [[0.0, 1.0], [1.0, 0.0]], expected: [[0.0], [1.0]] },
+ { input: [[1.0, 1.0], [1.0, 1.0]], expected: [[-3.0], [-3.0]] },
+ { input: [[-1.0, -1.0], [1.0, 1.0]], expected: [[3.0], [3.0]] },
+ { input: [[0.1, 0.1], [1.0, 1.0]], expected: [[hexToF32(0xbe99999a), hexToF32(0xbe999998)], [hexToF32(0xbe99999a), hexToF32(0xbe999998)]] }, // [~-0.3, ~-0.3]
+ { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max], [1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)]] }, // [~0.0, ~0.0]
+
+ // vec3s
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[-1.0], [0.0], [0.0]] },
+ { input: [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [1.0], [0.0]] },
+ { input: [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [1.0]] },
+ { input: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: [[1.0], [0.0], [0.0]] },
+ { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [[1.0], [0.0], [0.0]] },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [[-5.0], [-5.0], [-5.0]] },
+ { input: [[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]], expected: [[5.0], [5.0], [5.0]] },
+ { input: [[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]], expected: [[hexToF32(0xbf000001), hexToF32(0xbefffffe)], [hexToF32(0xbf000001), hexToF32(0xbefffffe)], [hexToF32(0xbf000001), hexToF32(0xbefffffe)]] }, // [~-0.5, ~-0.5, ~-0.5]
+ { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, 0.0], [1.0, 1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0]
+
+ // vec4s
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[-1.0], [0.0], [0.0], [0.0]] },
+ { input: [[0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [1.0], [0.0], [0.0]] },
+ { input: [[0.0, 0.0, 1.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [0.0], [1.0], [0.0]] },
+ { input: [[0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0], [1.0]] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [[1.0], [0.0], [0.0], [0.0]] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [[1.0], [0.0], [0.0], [0.0]] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [[1.0], [0.0], [0.0], [0.0]] },
+ { input: [[-1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]], expected: [[7.0], [7.0], [7.0], [7.0]] },
+ { input: [[0.1, 0.1, 0.1, 0.1], [1.0, 1.0, 1.0, 1.0]], expected: [[hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)]] }, // [~-0.7, ~-0.7, ~-0.7, ~-0.7]
+ { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0, ~0.0]
+
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] },
+
+ // Test that post-dot going OOB propagates
+ { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [kAny, kAny, kAny, kAny] },
+ ]
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const expected = toF32Vector(t.params.expected);
+
+ const got = reflectInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `reflectInterval([${x}], [${y}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface FaceForwardCase {
+ input: [number[], number[], number[]];
+ expected: (IntervalBounds[] | undefined)[];
+}
+
+g.test('faceForwardIntervals')
+ .paramsSubcasesOnly<FaceForwardCase>(
+ // prettier-ignore
+ [
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[[-1.0], [0.0]]] },
+ { input: [[-1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[[1.0], [0.0]]] },
+ { input: [[1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[[1.0], [0.0]]] },
+ { input: [[-1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[[-1.0], [0.0]]] },
+ { input: [[10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[[-10.0], [0.0]]] },
+ { input: [[-10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[[10.0], [0.0]]] },
+ { input: [[10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[[10.0], [0.0]]] },
+ { input: [[-10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[[-10.0], [0.0]]] },
+ { input: [[0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0]]] },
+ { input: [[-0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0]]] },
+ { input: [[0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0]]] },
+ { input: [[-0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0]]] },
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0]]] },
+ { input: [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0]]] },
+ { input: [[1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[[1.0], [0.0], [0.0]]] },
+ { input: [[-1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[[-1.0], [0.0], [0.0]]] },
+ { input: [[10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0]]] },
+ { input: [[-10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0]]] },
+ { input: [[10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[[10.0], [0.0], [0.0]]] },
+ { input: [[-10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[[-10.0], [0.0], [0.0]]] },
+ { input: [[0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0]]] },
+ { input: [[-0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0]]] },
+ { input: [[0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0]]] },
+ { input: [[-0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0]]] },
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0], [0.0]]] },
+ { input: [[-1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0], [0.0]]] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0], [0.0]]] },
+ { input: [[-1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0], [0.0]]] },
+ { input: [[10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0], [0.0]]] },
+ { input: [[-10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0], [0.0]]] },
+ { input: [[10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0], [0.0]]] },
+ { input: [[-10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0], [0.0]]] },
+ { input: [[0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0], [0.0]]] },
+ { input: [[-0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0], [0.0]]] },
+ { input: [[0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0], [0.0]]] },
+ { input: [[-0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0], [0.0]]] },
+
+ // dot(y, z) === 0
+ { input: [[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]], expected: [[[-1.0], [-1.0]]]},
+
+ // subnormals, also dot(y, z) spans 0
+ { input: [[kValue.f32.subnormal.positive.max, 0.0], [kValue.f32.subnormal.positive.min, 0.0], [kValue.f32.subnormal.negative.min, 0.0]], expected: [[[0.0, kValue.f32.subnormal.positive.max], [0.0]], [[kValue.f32.subnormal.negative.min, 0], [0.0]]] },
+
+ // dot going OOB returns [undefined, x, -x]
+ { input: [[1.0, 1.0], [kValue.f32.positive.max, kValue.f32.positive.max], [kValue.f32.positive.max, kValue.f32.positive.max]], expected: [undefined, [[1], [1]], [[-1], [-1]]] },
+
+ ]
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const expected = t.params.expected.map(e => (e !== undefined ? toF32Vector(e) : undefined));
+
+ const got = faceForwardIntervals(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `faceForwardInterval([${x}], [${y}], [${z}]) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+interface RefractCase {
+ input: [number[], number[], number];
+ expected: IntervalBounds[];
+}
+
+// Scope for refractInterval tests so that they can have constants for magic
+// numbers that don't pollute the global namespace or have unwieldy long names.
+{
+ const kNegativeOneBounds: IntervalBounds = [
+ hexToF64(0xbff00000, 0xc0000000),
+ hexToF64(0xbfefffff, 0x40000000),
+ ];
+
+ g.test('refractInterval')
+ .paramsSubcasesOnly<RefractCase>(
+ // Some of these are hard coded, since the error intervals are difficult
+ // to express in a closed human readable form due to the inherited nature
+ // of the errors.
+
+ // prettier-ignore
+ [
+ // k < 0
+ { input: [[1, 1], [0.1, 0], 10], expected: [[0], [0]] },
+
+ // k contains 0
+ { input: [[1, 1], [0.1, 0], 1.005038], expected: [kAny, kAny] },
+
+ // k > 0
+ // vec2
+ { input: [[1, 1], [1, 0], 1], expected: [kNegativeOneBounds, [1]] },
+ { input: [[1, -2], [3, 4], 5], expected: [[hexToF32(0x40ce87a4), hexToF32(0x40ce8840)], // ~6.454...
+ [hexToF32(0xc100fae8), hexToF32(0xc100fa80)]] }, // ~-8.061...
+
+ // vec3
+ { input: [[1, 1, 1], [1, 0, 0], 1], expected: [kNegativeOneBounds, [1], [1]] },
+ { input: [[1, -2, 3], [-4, 5, -6], 7], expected: [[hexToF32(0x40d24480), hexToF32(0x40d24c00)], // ~6.571...
+ [hexToF32(0xc1576f80), hexToF32(0xc1576ad0)], // ~-13.464...
+ [hexToF32(0x41a2d9b0), hexToF32(0x41a2dc80)]] }, // ~20.356...
+
+ // vec4
+ { input: [[1, 1, 1, 1], [1, 0, 0, 0], 1], expected: [kNegativeOneBounds, [1], [1], [1]] },
+ { input: [[1, -2, 3,-4], [-5, 6, -7, 8], 9], expected: [[hexToF32(0x410ae480), hexToF32(0x410af240)], // ~8.680...
+ [hexToF32(0xc18cf7c0), hexToF32(0xc18cef80)], // ~-17.620...
+ [hexToF32(0x41d46cc0), hexToF32(0x41d47660)], // ~26.553...
+ [hexToF32(0xc20dfa80), hexToF32(0xc20df500)]] }, // ~-35.494...
+
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] },
+ ]
+ )
+ .fn(t => {
+ const [i, s, r] = t.params.input;
+ const expected = toF32Vector(t.params.expected);
+
+ const got = refractInterval(i, s, r);
+ t.expect(
+ objectEquals(expected, got),
+ `refractIntervals([${i}], [${s}], ${r}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+}
+
+interface ModfCase {
+ input: number;
+ fract: IntervalBounds;
+ whole: IntervalBounds;
+}
+
+g.test('modfInterval')
+ .paramsSubcasesOnly<ModfCase>(
+ // prettier-ignore
+ [
+ // Normals
+ { input: 0, fract: [0], whole: [0] },
+ { input: 1, fract: [0], whole: [1] },
+ { input: -1, fract: [0], whole: [-1] },
+ { input: 0.5, fract: [0.5], whole: [0] },
+ { input: -0.5, fract: [-0.5], whole: [0] },
+ { input: 2.5, fract: [0.5], whole: [2] },
+ { input: -2.5, fract: [-0.5], whole: [-2] },
+ { input: 10.0, fract: [0], whole: [10] },
+ { input: -10.0, fract: [0], whole: [-10] },
+
+ // Subnormals
+ { input: kValue.f32.subnormal.negative.min, fract: [kValue.f32.subnormal.negative.min, 0], whole: [0] },
+ { input: kValue.f32.subnormal.negative.max, fract: [kValue.f32.subnormal.negative.max, 0], whole: [0] },
+ { input: kValue.f32.subnormal.positive.min, fract: [0, kValue.f32.subnormal.positive.min], whole: [0] },
+ { input: kValue.f32.subnormal.positive.max, fract: [0, kValue.f32.subnormal.positive.max], whole: [0] },
+
+ // Boundaries
+ { input: kValue.f32.negative.min, fract: [0], whole: [kValue.f32.negative.min] },
+ { input: kValue.f32.negative.max, fract: [kValue.f32.negative.max], whole: [0] },
+ { input: kValue.f32.positive.min, fract: [kValue.f32.positive.min], whole: [0] },
+ { input: kValue.f32.positive.max, fract: [0], whole: [kValue.f32.positive.max] },
+ ]
+ )
+ .fn(t => {
+ const expected = {
+ fract: toF32Interval(t.params.fract),
+ whole: toF32Interval(t.params.whole),
+ };
+
+ const got = modfInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `modfInterval([${t.params.input}) returned { fract: [${got.fract}], whole: [${got.whole}] }. Expected { fract: [${expected.fract}], whole: [${expected.whole}] }`
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts
new file mode 100644
index 0000000000..5090fe3f9d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts
@@ -0,0 +1,138 @@
+export const description = `
+Tests for getStackTrace.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { extractImportantStackTrace } from '../common/internal/stack.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('stacks')
+ .paramsSimple([
+ {
+ case: 'node_fail',
+ _expectedLines: 3,
+ _stack: `Error:
+ at CaseRecorder.fail (/Users/kainino/src/cts/src/common/framework/logger.ts:99:30)
+ at RunCaseSpecific.exports.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/logger.spec.ts:80:7)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+ at processTicksAndRejections (internal/process/task_queues.js:86:5)`,
+ },
+ {
+ // MAINTENANCE_TODO: make sure this test case actually matches what happens on windows
+ case: 'node_fail_backslash',
+ _expectedLines: 3,
+ _stack: `Error:
+ at CaseRecorder.fail (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\logger.ts:99:30)
+ at RunCaseSpecific.exports.g.test.t [as fn] (C:\\Users\\kainino\\src\\cts\\src\\unittests\\logger.spec.ts:80:7)
+ at RunCaseSpecific.run (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\test_group.ts:121:18)
+ at processTicksAndRejections (internal\\process\\task_queues.js:86:5)`,
+ },
+ {
+ case: 'node_fail_processTicksAndRejections',
+ _expectedLines: 5,
+ _stack: `Error: expectation had no effect: suite1:foo:
+ at Object.generateMinimalQueryList (/Users/kainino/src/cts/src/common/framework/generate_minimal_query_list.ts:72:24)
+ at testGenerateMinimalQueryList (/Users/kainino/src/cts/src/unittests/loading.spec.ts:289:25)
+ at processTicksAndRejections (internal/process/task_queues.js:93:5)
+ at RunCaseSpecific.fn (/Users/kainino/src/cts/src/unittests/loading.spec.ts:300:3)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:144:9)
+ at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:62:25
+ at async Promise.all (index 29)
+ at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:78:5`,
+ },
+ {
+ case: 'node_throw',
+ _expectedLines: 2,
+ _stack: `Error: hello
+ at RunCaseSpecific.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/test_group.spec.ts:51:11)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+ at processTicksAndRejections (internal/process/task_queues.js:86:5)`,
+ },
+ {
+ case: 'firefox_fail',
+ _expectedLines: 3,
+ _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:30
+expect@http://localhost:8080/out/common/framework/default_fixture.js:59:16
+@http://localhost:8080/out/unittests/util.spec.js:35:5
+run@http://localhost:8080/out/common/framework/test_group.js:119:18`,
+ },
+ {
+ case: 'firefox_throw',
+ _expectedLines: 1,
+ _stack: `@http://localhost:8080/out/unittests/test_group.spec.js:48:11
+run@http://localhost:8080/out/common/framework/test_group.js:119:18`,
+ },
+ {
+ case: 'safari_fail',
+ _expectedLines: 3,
+ _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:39
+expect@http://localhost:8080/out/common/framework/default_fixture.js:59:20
+http://localhost:8080/out/unittests/util.spec.js:35:11
+http://localhost:8080/out/common/framework/test_group.js:119:20
+asyncFunctionResume@[native code]
+[native code]
+promiseReactionJob@[native code]`,
+ },
+ {
+ case: 'safari_throw',
+ _expectedLines: 1,
+ _stack: `http://localhost:8080/out/unittests/test_group.spec.js:48:20
+http://localhost:8080/out/common/framework/test_group.js:119:20
+asyncFunctionResume@[native code]
+[native code]
+promiseReactionJob@[native code]`,
+ },
+ {
+ case: 'chrome_fail',
+ _expectedLines: 4,
+ _stack: `Error
+ at CaseRecorder.fail (http://localhost:8080/out/common/framework/logger.js:104:30)
+ at DefaultFixture.expect (http://localhost:8080/out/common/framework/default_fixture.js:59:16)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/util.spec.js:35:5)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ {
+ case: 'chrome_throw',
+ _expectedLines: 6,
+ _stack: `Error: hello
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)"
+ at async Promise.all (index 0)
+ at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
+ at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
+ at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ {
+ case: 'multiple_lines',
+ _expectedLines: 8,
+ _stack: `Error: hello
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)"
+ at async Promise.all (index 0)
+ at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
+ at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
+ at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ ])
+ .fn(t => {
+ const ex = new Error();
+ ex.stack = t.params._stack;
+ t.expect(ex.stack === t.params._stack);
+ const stringified = extractImportantStackTrace(ex);
+ const parts = stringified.split('\n');
+
+ t.expect(parts.length === t.params._expectedLines);
+ const last = parts[parts.length - 1];
+ t.expect(last.indexOf('/unittests/') !== -1 || last.indexOf('\\unittests\\') !== -1);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts
new file mode 100644
index 0000000000..823639c692
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/no-restricted-paths */
+import { TestSuiteListing } from '../common/internal/test_suite_listing.js';
+import { makeListing } from '../common/tools/crawl.js';
+
+export const listing: Promise<TestSuiteListing> = makeListing(__filename);
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts
new file mode 100644
index 0000000000..29d0b7442c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts
@@ -0,0 +1,931 @@
+export const description = `
+Tests for queries/filtering, loading, and running.
+`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { TestFileLoader, SpecFile } from '../common/internal/file_loader.js';
+import { Logger } from '../common/internal/logging/logger.js';
+import { Status } from '../common/internal/logging/result.js';
+import { parseQuery } from '../common/internal/query/parseQuery.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiCase,
+ TestQueryMultiTest,
+ TestQueryMultiFile,
+ TestQueryWithExpectation,
+} from '../common/internal/query/query.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { TestSuiteListing, TestSuiteListingEntry } from '../common/internal/test_suite_listing.js';
+import { ExpandThroughLevel, TestTreeLeaf } from '../common/internal/tree.js';
+import { assert, objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+const listingData: { [k: string]: TestSuiteListingEntry[] } = {
+ suite1: [
+ { file: [], readme: 'desc 1a' },
+ { file: ['foo'] },
+ { file: ['bar'], readme: 'desc 1h' },
+ { file: ['bar', 'biz'] },
+ { file: ['bar', 'buzz', 'buzz'] },
+ { file: ['baz'] },
+ { file: ['empty'], readme: 'desc 1z' }, // directory with no files
+ ],
+ suite2: [{ file: [], readme: 'desc 2a' }, { file: ['foof'] }],
+};
+
+const specsData: { [k: string]: SpecFile } = {
+ 'suite1/foo.spec.js': {
+ description: 'desc 1b',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('hello').fn(() => {});
+ g.test('bonjour').fn(() => {});
+ g.test('hola')
+ .desc('TODO TODO')
+ .fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite1/bar/biz.spec.js': {
+ description: 'desc 1f TODO TODO',
+ g: makeTestGroupForUnitTesting(UnitTest), // file with no tests
+ },
+ 'suite1/bar/buzz/buzz.spec.js': {
+ description: 'desc 1d TODO',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('zap').fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite1/baz.spec.js': {
+ description: 'desc 1e',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('wye')
+ .paramsSimple([{}, { x: 1 }])
+ .fn(() => {});
+ g.test('zed')
+ .paramsSimple([
+ { a: 1, b: 2, _c: 0 },
+ { b: 3, a: 1, _c: 0 },
+ ])
+ .fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite2/foof.spec.js': {
+ description: 'desc 2b',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('blah').fn(t => {
+ t.debug('OK');
+ });
+ g.test('bleh')
+ .paramsSimple([{ a: 1 }])
+ .fn(t => {
+ t.debug('OK');
+ t.debug('OK');
+ });
+ g.test('bluh,a').fn(t => {
+ t.fail('goodbye');
+ });
+ return g;
+ })(),
+ },
+};
+
+class FakeTestFileLoader extends TestFileLoader {
+ async listing(suite: string): Promise<TestSuiteListing> {
+ return listingData[suite];
+ }
+
+ async import(path: string): Promise<SpecFile> {
+ assert(path in specsData, '[test] mock file ' + path + ' does not exist');
+ return specsData[path];
+ }
+}
+
+class LoadingTest extends UnitTest {
+ loader: FakeTestFileLoader = new FakeTestFileLoader();
+ events: (string | null)[] = [];
+ private isListenersAdded = false;
+
+ collectEvents(): void {
+ this.events = [];
+ if (!this.isListenersAdded) {
+ this.isListenersAdded = true;
+ this.loader.addEventListener('import', ev => this.events.push(ev.data.url));
+ this.loader.addEventListener('finish', ev => this.events.push(null));
+ }
+ }
+
+ async load(query: string): Promise<TestTreeLeaf[]> {
+ return Array.from(await this.loader.loadCases(parseQuery(query)));
+ }
+
+ async loadNames(query: string): Promise<string[]> {
+ return (await this.load(query)).map(c => c.query.toString());
+ }
+}
+
+export const g = makeTestGroup(LoadingTest);
+
+g.test('suite').fn(async t => {
+ t.shouldReject('Error', t.load('suite1'));
+ t.shouldReject('Error', t.load('suite1:'));
+});
+
+g.test('group').fn(async t => {
+ t.collectEvents();
+ t.expect((await t.load('suite1:*')).length === 8);
+ t.expect(
+ objectEquals(t.events, [
+ 'suite1/foo.spec.js',
+ 'suite1/bar/biz.spec.js',
+ 'suite1/bar/buzz/buzz.spec.js',
+ 'suite1/baz.spec.js',
+ null,
+ ])
+ );
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:foo,*')).length === 3); // x:foo,* matches x:foo:
+ t.expect(objectEquals(t.events, ['suite1/foo.spec.js', null]));
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:bar,*')).length === 1);
+ t.expect(
+ objectEquals(t.events, ['suite1/bar/biz.spec.js', 'suite1/bar/buzz/buzz.spec.js', null])
+ );
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:bar,buzz,buzz,*')).length === 1);
+ t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null]));
+
+ t.shouldReject('Error', t.load('suite1:f*'));
+
+ {
+ const s = new TestQueryMultiFile('suite1', ['bar', 'buzz']).toString();
+ t.collectEvents();
+ t.expect((await t.load(s)).length === 1);
+ t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null]));
+ }
+});
+
+g.test('test').fn(async t => {
+ t.shouldReject('Error', t.load('suite1::'));
+ t.shouldReject('Error', t.load('suite1:bar:'));
+ t.shouldReject('Error', t.load('suite1:bar,:'));
+
+ t.shouldReject('Error', t.load('suite1::*'));
+ t.shouldReject('Error', t.load('suite1:bar,:*'));
+ t.shouldReject('Error', t.load('suite1:bar:*'));
+
+ t.expect((await t.load('suite1:foo:*')).length === 3);
+ t.expect((await t.load('suite1:bar,buzz,buzz:*')).length === 1);
+ t.expect((await t.load('suite1:baz:*')).length === 4);
+
+ t.expect((await t.load('suite2:foof:bluh,*')).length === 1);
+ t.expect((await t.load('suite2:foof:bluh,a,*')).length === 1);
+
+ {
+ const s = new TestQueryMultiTest('suite2', ['foof'], ['bluh']).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+});
+
+g.test('case').fn(async t => {
+ t.shouldReject('Error', t.load('suite1:foo::'));
+ t.shouldReject('Error', t.load('suite1:bar:zed,:'));
+
+ t.shouldReject('Error', t.load('suite1:foo:h*'));
+
+ t.shouldReject('Error', t.load('suite1:foo::*'));
+ t.shouldReject('Error', t.load('suite1:baz::*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed,:*'));
+
+ t.shouldReject('Error', t.load('suite1:baz:zed:'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2;'));
+ t.shouldReject('SyntaxError', t.load('suite1:baz:zed:a=1;b=2,')); // tries to parse '2,' as JSON
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1,b=2')); // '=' not allowed in value '1,b=2'
+ t.shouldReject('Error', t.load('suite1:baz:zed:b=2*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:b=2;a=1;_c=0'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1,*'));
+
+ t.expect((await t.load('suite1:baz:zed:*')).length === 2);
+ t.expect((await t.load('suite1:baz:zed:a=1;*')).length === 2);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=2')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=2;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;a=1')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;a=1;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=3;a=1')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=3')).length === 1);
+ t.expect((await t.load('suite1:foo:hello:')).length === 1);
+
+ {
+ const s = new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+ {
+ const s = new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+});
+
+async function runTestcase(
+ t: Fixture,
+ log: Logger,
+ testcases: TestTreeLeaf[],
+ i: number,
+ query: TestQuery,
+ expectations: TestQueryWithExpectation[],
+ status: Status,
+ logs: (s: string[]) => boolean
+) {
+ t.expect(objectEquals(testcases[i].query, query));
+ const name = testcases[i].query.toString();
+ const [rec, res] = log.record(name);
+ await testcases[i].run(rec, expectations);
+
+ t.expect(log.results.get(name) === res);
+ t.expect(res.status === status);
+ t.expect(res.timems >= 0);
+ assert(res.logs !== undefined); // only undefined while pending
+ t.expect(logs(res.logs.map(l => JSON.stringify(l))));
+}
+
+g.test('end2end').fn(async t => {
+ const l = await t.load('suite2:foof:*');
+ assert(l.length === 3, 'listing length');
+
+ const log = new Logger({ overrideDebugMode: true });
+
+ await runTestcase(
+ t,
+ log,
+ l,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ [],
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"'])
+ );
+ await runTestcase(
+ t,
+ log,
+ l,
+ 1,
+ new TestQuerySingleCase('suite2', ['foof'], ['bleh'], { a: 1 }),
+ [],
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"', '"DEBUG: OK"'])
+ );
+ await runTestcase(
+ t,
+ log,
+ l,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ [],
+ 'fail',
+ logs =>
+ logs.length === 1 &&
+ logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n') &&
+ logs[0].indexOf('loaders_and_trees.spec.') !== -1
+ );
+});
+
+g.test('expectations,single_case').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+
+ // Single-case. Covers one case.
+ const zedExpectationsSkipA1B2 = [
+ {
+ query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1B2,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1B2,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,single_case,none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Single-case. Doesn't cover any cases.
+ const zedExpectationsSkipA1B0 = [
+ {
+ query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 0 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1B0,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1B0,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,multi_case').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, not all cases covered.
+ const zedExpectationsSkipB3 = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { b: 3 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipB3,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipB3,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_case_all').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, all cases covered.
+ const zedExpectationsSkipA1 = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_case_none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, no params, all cases covered.
+ const zedExpectationsSkipZed = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipZed,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipZed,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_test').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-test, all cases covered.
+ const expectationsSkipAllInBaz = [
+ {
+ query: new TestQueryMultiTest('suite1', ['baz'], []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 4,
+ new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}),
+ expectationsSkipAllInBaz,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 6,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectationsSkipAllInBaz,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_test,none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-test, no cases covered.
+ const expectationsSkipAllInFoo = [
+ {
+ query: new TestQueryMultiTest('suite1', ['foo'], []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 4,
+ new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}),
+ expectationsSkipAllInFoo,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 6,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectationsSkipAllInFoo,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,multi_file').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-file
+ const expectationsSkipAll = [
+ {
+ query: new TestQueryMultiFile('suite1', []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 0,
+ new TestQuerySingleCase('suite1', ['foo'], ['hello'], {}),
+ expectationsSkipAll,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 3,
+ new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}),
+ expectationsSkipAll,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,catches_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ // Catches failure
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'fail' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ expectedFailures,
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"'])
+ );
+
+ // Status is passed, but failure is logged.
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'pass',
+ logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n')
+ );
+});
+
+g.test('expectations,skip_dominates_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'fail' as const,
+ },
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,skip_inside_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiFile('suite2', []),
+ expectation: 'fail' as const,
+ },
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['blah'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ expectedFailures,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'pass',
+ logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n')
+ );
+});
+
+async function testIterateCollapsed(
+ t: LoadingTest,
+ alwaysExpandThroughLevel: ExpandThroughLevel,
+ expectations: string[],
+ expectedResult: 'throws' | string[] | [string, number | undefined][],
+ includeEmptySubtrees = false
+) {
+ t.debug(`expandThrough=${alwaysExpandThroughLevel} expectations=${expectations}`);
+ const treePromise = t.loader.loadTree(new TestQueryMultiFile('suite1', []), expectations);
+ if (expectedResult === 'throws') {
+ t.shouldReject('Error', treePromise, 'loadTree should have thrown Error');
+ return;
+ }
+ const tree = await treePromise;
+ const actualIter = tree.iterateCollapsedNodes({
+ includeEmptySubtrees,
+ alwaysExpandThroughLevel,
+ });
+ const testingTODOs = expectedResult.length > 0 && expectedResult[0] instanceof Array;
+ const actual = Array.from(actualIter, ({ query, subtreeCounts }) =>
+ testingTODOs ? [query.toString(), subtreeCounts?.nodesWithTODO] : query.toString()
+ );
+ if (!objectEquals(actual, expectedResult)) {
+ t.fail(
+ `iterateCollapsed failed:
+ got ${JSON.stringify(actual)}
+ exp ${JSON.stringify(expectedResult)}
+${tree.toString()}`
+ );
+ }
+}
+
+g.test('print').fn(async t => {
+ const tree = await t.loader.loadTree(new TestQueryMultiFile('suite1', []));
+ tree.toString();
+});
+
+g.test('iterateCollapsed').fn(async t => {
+ await testIterateCollapsed(
+ t,
+ 1,
+ [],
+ [
+ ['suite1:foo:*', 1], // to-do propagated up from foo:hola
+ ['suite1:bar,buzz,buzz:*', 1], // to-do in file description
+ ['suite1:baz:*', 0],
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ [],
+ [
+ ['suite1:foo:hello:*', 0],
+ ['suite1:foo:bonjour:*', 0],
+ ['suite1:foo:hola:*', 1], // to-do in test description
+ ['suite1:bar,buzz,buzz:zap:*', 0],
+ ['suite1:baz:wye:*', 0],
+ ['suite1:baz:zed:*', 0],
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 3,
+ [],
+ [
+ ['suite1:foo:hello:', undefined],
+ ['suite1:foo:bonjour:', undefined],
+ ['suite1:foo:hola:', undefined],
+ ['suite1:bar,buzz,buzz:zap:', undefined],
+ ['suite1:baz:wye:', undefined],
+ ['suite1:baz:wye:x=1', undefined],
+ ['suite1:baz:zed:a=1;b=2', undefined],
+ ['suite1:baz:zed:b=3;a=1', undefined],
+ ]
+ );
+
+ // Expectations lists that have no effect
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:foo:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:bar,buzz,buzz:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:*'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed:*',
+ ]
+ );
+ // Test with includeEmptySubtrees=true
+ await testIterateCollapsed(
+ t,
+ 1,
+ [],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,biz:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:*',
+ 'suite1:empty,*',
+ ],
+ true
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ [],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,biz:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed:*',
+ 'suite1:empty,*',
+ ],
+ true
+ );
+
+ // Expectations lists that have some effect
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye:*', 'suite1:baz:zed,*']
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:zed:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye,*', 'suite1:baz:zed:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:*', 'suite1:baz:zed:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye:*', 'suite1:baz:zed:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:x=1'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1',
+ 'suite1:baz:zed,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:foo:*', 'suite1:baz:wye:'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed:*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:x=1'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1',
+ 'suite1:baz:zed:*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:foo:hello:*', 'suite1:baz:wye:'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed:*',
+ ]
+ );
+
+ // Invalid expectation queries
+ await testIterateCollapsed(t, 1, ['*'], 'throws');
+ await testIterateCollapsed(t, 1, ['garbage'], 'throws');
+ await testIterateCollapsed(t, 1, ['garbage*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:foo*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:foo:he*'], 'throws');
+
+ // Valid expectation queries but they don't match anything
+ await testIterateCollapsed(t, 1, ['garbage:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:doesntexist:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite2:foo:*'], 'throws');
+ // Can't expand subqueries bigger than one file.
+ await testIterateCollapsed(t, 1, ['suite1:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:bar,*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:bar:hello,*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:baz,*'], 'throws');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts
new file mode 100644
index 0000000000..18aa0a02fe
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts
@@ -0,0 +1,147 @@
+export const description = `
+Unit tests for namespaced logging system.
+
+Also serves as a larger test of async test functions, and of the logging system.
+`;
+
+import { SkipTestCase } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { Logger } from '../common/internal/logging/logger.js';
+import { assert } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('construct').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [, res1] = mylog.record('one');
+ const [, res2] = mylog.record('two');
+
+ t.expect(mylog.results.get('one') === res1);
+ t.expect(mylog.results.get('two') === res2);
+ t.expect(res1.logs === undefined);
+ t.expect(res1.status === 'running');
+ t.expect(res1.timems < 0);
+ t.expect(res2.logs === undefined);
+ t.expect(res2.status === 'running');
+ t.expect(res2.timems < 0);
+});
+
+g.test('empty').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ t.expect(res.status === 'running');
+ rec.finish();
+
+ t.expect(res.status === 'pass');
+ t.expect(res.timems >= 0);
+});
+
+g.test('pass').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.debug(new Error('hello'));
+ t.expect(res.status === 'running');
+ rec.finish();
+
+ t.expect(res.status === 'pass');
+ t.expect(res.timems >= 0);
+});
+
+g.test('skip').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.skipped(new SkipTestCase());
+ rec.debug(new Error('hello'));
+ rec.finish();
+
+ t.expect(res.status === 'skip');
+ t.expect(res.timems >= 0);
+});
+
+g.test('warn').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.warn(new Error('hello'));
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'warn');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,expectationFailed').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.expectationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,validationFailed').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.validationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,threw').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.threw(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('debug')
+ .paramsSimple([
+ { debug: true, _logsCount: 5 }, //
+ { debug: false, _logsCount: 3 },
+ ])
+ .fn(t => {
+ const { debug, _logsCount } = t.params;
+
+ const mylog = new Logger({ overrideDebugMode: debug });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.debug(new Error('hello'));
+ rec.expectationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.debug(new Error('foo'));
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+ assert(res.logs !== undefined);
+ t.expect(res.logs.length === _logsCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts
new file mode 100644
index 0000000000..4f3181855b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts
@@ -0,0 +1,1021 @@
+export const description = `
+Util math unit tests.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import { kBit, kValue } from '../webgpu/util/constants.js';
+import {
+ f32,
+ f32Bits,
+ float16ToUint16,
+ float32ToUint32,
+ Scalar,
+ uint16ToFloat16,
+ uint32ToFloat32,
+} from '../webgpu/util/conversion.js';
+import {
+ biasedRange,
+ calculatePermutations,
+ cartesianProduct,
+ correctlyRoundedF32,
+ FlushMode,
+ fullF16Range,
+ fullF32Range,
+ fullI32Range,
+ hexToF32,
+ hexToF64,
+ lerp,
+ linearRange,
+ nextAfterF32,
+ oneULP,
+} from '../webgpu/util/math.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+/**
+ * Utility wrapper around oneULP to test if a value is within 1 ULP(x)
+ *
+ * @param got number to test
+ * @param expected number to be within 1 ULP of
+ * @param mode should oneULP FTZ
+ * @returns if got is within 1 ULP of expected
+ */
+function withinOneULP(got: number, expected: number, mode: FlushMode): boolean {
+ const ulp = oneULP(expected, mode);
+ return got >= expected - ulp && got <= expected + ulp;
+}
+
+/**
+ * @returns true if arrays are equal within 1ULP, doing element-wise comparison
+ * as needed, and considering NaNs to be equal.
+ *
+ * Depends on the correctness of oneULP, which is tested in this file.
+ **
+ * @param got array of numbers to compare for equality
+ * @param expect array of numbers to compare against
+ * @param mode should different subnormals be considered the same, i.e. should
+ * FTZ occur during comparison
+ **/
+function compareArrayOfNumbers(
+ got: Array<number>,
+ expect: Array<number>,
+ mode: FlushMode = 'flush'
+): boolean {
+ return (
+ got.length === expect.length &&
+ got.every((value, index) => {
+ const expected = expect[index];
+ return (Number.isNaN(value) && Number.isNaN(expected)) || withinOneULP(value, expected, mode);
+ })
+ );
+}
+
+interface nextAfterCase {
+ val: number;
+ dir: boolean;
+ result: Scalar;
+}
+
+g.test('nextAfterFlushToZero')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: true, result: f32Bits(0x7fffffff) },
+ { val: Number.NaN, dir: false, result: f32Bits(0x7fffffff) },
+ { val: Number.POSITIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: Number.POSITIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: Number.NEGATIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.negative) },
+ { val: Number.NEGATIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.negative) },
+
+ // Zeroes
+ { val: +0, dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: +0, dir: false, result: f32Bits(kBit.f32.negative.max) },
+ { val: -0, dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: -0, dir: false, result: f32Bits(kBit.f32.negative.max) },
+
+ // Subnormals
+ { val: hexToF32(kBit.f32.subnormal.positive.min), dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: hexToF32(kBit.f32.subnormal.positive.min), dir: false, result: f32Bits(kBit.f32.negative.max) },
+ { val: hexToF32(kBit.f32.subnormal.positive.max), dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: hexToF32(kBit.f32.subnormal.positive.max), dir: false, result: f32Bits(kBit.f32.negative.max) },
+ { val: hexToF32(kBit.f32.subnormal.negative.min), dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: hexToF32(kBit.f32.subnormal.negative.min), dir: false, result: f32Bits(kBit.f32.negative.max) },
+ { val: hexToF32(kBit.f32.subnormal.negative.max), dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: hexToF32(kBit.f32.subnormal.negative.max), dir: false, result: f32Bits(kBit.f32.negative.max) },
+
+ // Normals
+ { val: hexToF32(kBit.f32.positive.max), dir: true, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: hexToF32(kBit.f32.positive.max), dir: false, result: f32Bits(0x7f7ffffe) },
+ { val: hexToF32(kBit.f32.positive.min), dir: true, result: f32Bits(0x00800001) },
+ { val: hexToF32(kBit.f32.positive.min), dir: false, result: f32(0) },
+ { val: hexToF32(kBit.f32.negative.max), dir: true, result: f32(0) },
+ { val: hexToF32(kBit.f32.negative.max), dir: false, result: f32Bits(0x80800001) },
+ { val: hexToF32(kBit.f32.negative.min), dir: true, result: f32Bits(0xff7ffffe) },
+ { val: hexToF32(kBit.f32.negative.min), dir: false, result: f32Bits(kBit.f32.infinity.negative) },
+ { val: hexToF32(0x03800000), dir: true, result: f32Bits(0x03800001) },
+ { val: hexToF32(0x03800000), dir: false, result: f32Bits(0x037fffff) },
+ { val: hexToF32(0x83800000), dir: true, result: f32Bits(0x837fffff) },
+ { val: hexToF32(0x83800000), dir: false, result: f32Bits(0x83800001) },
+
+ // Not precisely expressible as float32
+ { val: 0.001, dir: true, result: f32Bits(0x3a83126f) }, // positive normal
+ { val: 0.001, dir: false, result: f32Bits(0x3a83126e) }, // positive normal
+ { val: -0.001, dir: true, result: f32Bits(0xba83126e) }, // negative normal
+ { val: -0.001, dir: false, result: f32Bits(0xba83126f) }, // negative normal
+ { val: 2.82E-40, dir: true, result: f32Bits(kBit.f32.positive.min) }, // positive subnormal
+ { val: 2.82E-40, dir: false, result: f32Bits(kBit.f32.negative.max) }, // positive subnormal
+ { val: -2.82E-40, dir: true, result: f32Bits(kBit.f32.positive.min) }, // negative subnormal
+ { val: -2.82E-40, dir: false, result: f32Bits(kBit.f32.negative.max) }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const expect_type = typeof expect;
+ const got = nextAfterF32(val, dir, 'flush');
+ const got_type = typeof got;
+ t.expect(
+ got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)),
+ `nextAfter(${val}, ${dir}, true) returned ${got} (${got_type}). Expected ${expect} (${expect_type})`
+ );
+ });
+
+g.test('nextAfterNoFlush')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: true, result: f32Bits(0x7fffffff) },
+ { val: Number.NaN, dir: false, result: f32Bits(0x7fffffff) },
+ { val: Number.POSITIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: Number.POSITIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: Number.NEGATIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.negative) },
+ { val: Number.NEGATIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.negative) },
+
+ // Zeroes
+ { val: +0, dir: true, result: f32Bits(kBit.f32.subnormal.positive.min) },
+ { val: +0, dir: false, result: f32Bits(kBit.f32.subnormal.negative.max) },
+ { val: -0, dir: true, result: f32Bits(kBit.f32.subnormal.positive.min) },
+ { val: -0, dir: false, result: f32Bits(kBit.f32.subnormal.negative.max) },
+
+ // Subnormals
+ { val: hexToF32(kBit.f32.subnormal.positive.min), dir: true, result: f32Bits(0x00000002) },
+ { val: hexToF32(kBit.f32.subnormal.positive.min), dir: false, result: f32(0) },
+ { val: hexToF32(kBit.f32.subnormal.positive.max), dir: true, result: f32Bits(kBit.f32.positive.min) },
+ { val: hexToF32(kBit.f32.subnormal.positive.max), dir: false, result: f32Bits(0x007ffffe) },
+ { val: hexToF32(kBit.f32.subnormal.negative.min), dir: true, result: f32Bits(0x807ffffe) },
+ { val: hexToF32(kBit.f32.subnormal.negative.min), dir: false, result: f32Bits(kBit.f32.negative.max) },
+ { val: hexToF32(kBit.f32.subnormal.negative.max), dir: true, result: f32(0) },
+ { val: hexToF32(kBit.f32.subnormal.negative.max), dir: false, result: f32Bits(0x80000002) },
+
+ // Normals
+ { val: hexToF32(kBit.f32.positive.max), dir: true, result: f32Bits(kBit.f32.infinity.positive) },
+ { val: hexToF32(kBit.f32.positive.max), dir: false, result: f32Bits(0x7f7ffffe) },
+ { val: hexToF32(kBit.f32.positive.min), dir: true, result: f32Bits(0x00800001) },
+ { val: hexToF32(kBit.f32.positive.min), dir: false, result: f32Bits(kBit.f32.subnormal.positive.max) },
+ { val: hexToF32(kBit.f32.negative.max), dir: true, result: f32Bits(kBit.f32.subnormal.negative.min) },
+ { val: hexToF32(kBit.f32.negative.max), dir: false, result: f32Bits(0x80800001) },
+ { val: hexToF32(kBit.f32.negative.min), dir: true, result: f32Bits(0xff7ffffe) },
+ { val: hexToF32(kBit.f32.negative.min), dir: false, result: f32Bits(kBit.f32.infinity.negative) },
+ { val: hexToF32(0x03800000), dir: true, result: f32Bits(0x03800001) },
+ { val: hexToF32(0x03800000), dir: false, result: f32Bits(0x037fffff) },
+ { val: hexToF32(0x83800000), dir: true, result: f32Bits(0x837fffff) },
+ { val: hexToF32(0x83800000), dir: false, result: f32Bits(0x83800001) },
+
+ // Not precisely expressible as float32
+ { val: 0.001, dir: true, result: f32Bits(0x3a83126f) }, // positive normal
+ { val: 0.001, dir: false, result: f32Bits(0x3a83126e) }, // positive normal
+ { val: -0.001, dir: true, result: f32Bits(0xba83126e) }, // negative normal
+ { val: -0.001, dir: false, result: f32Bits(0xba83126f) }, // negative normal
+ { val: 2.82E-40, dir: true, result: f32Bits(0x0003121a) }, // positive subnormal
+ { val: 2.82E-40, dir: false, result: f32Bits(0x00031219) }, // positive subnormal
+ { val: -2.82E-40, dir: true, result: f32Bits(0x80031219) }, // negative subnormal
+ { val: -2.82E-40, dir: false, result: f32Bits(0x8003121a) }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const expect_type = typeof expect;
+ const got = nextAfterF32(val, dir, 'no-flush');
+ const got_type = typeof got;
+ t.expect(
+ got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)),
+ `nextAfter(${val}, ${dir}, false) returned ${got} (${got_type}). Expected ${expect} (${expect_type})`
+ );
+ });
+
+interface OneULPCase {
+ target: number;
+ expect: number;
+}
+
+g.test('oneULPFlushToZero')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) },
+ { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: hexToF32(0x00800000) },
+ { target: -0, expect: hexToF32(0x00800000) },
+
+ // Subnormals
+ { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00800000) },
+ { target: 2.82e-40, expect: hexToF32(0x00800000) }, // positive subnormal
+ { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00800000) },
+ { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00800000) },
+ { target: -2.82e-40, expect: hexToF32(0x00800000) }, // negative subnormal
+ { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00800000) },
+
+ // Normals
+ { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) },
+ { target: 1, expect: hexToF32(0x33800000) },
+ { target: 2, expect: hexToF32(0x34000000) },
+ { target: 4, expect: hexToF32(0x34800000) },
+ { target: 1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) },
+ { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x00000001) },
+ { target: -1, expect: hexToF32(0x33800000) },
+ { target: -2, expect: hexToF32(0x34000000) },
+ { target: -4, expect: hexToF32(0x34800000) },
+ { target: -1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) },
+
+ // No precise f32 value
+ { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal
+ { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal
+ { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range
+ { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULP(target, 'flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULP(${target}, true) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPNoFlush')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) },
+ { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: hexToF32(0x00000001) },
+ { target: -0, expect: hexToF32(0x00000001) },
+
+ // Subnormals
+ { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00000001) },
+ { target: -2.82e-40, expect: hexToF32(0x00000001) }, // negative subnormal
+ { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00000001) },
+ { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00000001) },
+ { target: 2.82e-40, expect: hexToF32(0x00000001) }, // positive subnormal
+ { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00000001) },
+
+ // Normals
+ { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) },
+ { target: 1, expect: hexToF32(0x33800000) },
+ { target: 2, expect: hexToF32(0x34000000) },
+ { target: 4, expect: hexToF32(0x34800000) },
+ { target: 1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) },
+ { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x00000001) },
+ { target: -1, expect: hexToF32(0x33800000) },
+ { target: -2, expect: hexToF32(0x34000000) },
+ { target: -4, expect: hexToF32(0x34800000) },
+ { target: -1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) },
+
+ // No precise f32 value
+ { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal
+ { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal
+ { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range
+ { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULP(target, 'no-flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPImpl(${target}, false) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULP')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) },
+ { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: hexToF32(0x00800000) },
+ { target: -0, expect: hexToF32(0x00800000) },
+
+ // Subnormals
+ { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00800000) },
+ { target: -2.82e-40, expect: hexToF32(0x00800000) },
+ { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00800000) },
+ { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00800000) },
+ { target: 2.82e-40, expect: hexToF32(0x00800000) },
+ { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00800000) },
+
+ // Normals
+ { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) },
+ { target: 1, expect: hexToF32(0x33800000) },
+ { target: 2, expect: hexToF32(0x34000000) },
+ { target: 4, expect: hexToF32(0x34800000) },
+ { target: 1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) },
+ { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x000000001) },
+ { target: -1, expect: hexToF32(0x33800000) },
+ { target: -2, expect: hexToF32(0x34000000) },
+ { target: -4, expect: hexToF32(0x34800000) },
+ { target: -1000000, expect: hexToF32(0x3d800000) },
+ { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) },
+
+ // No precise f32 value
+ { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal
+ { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range
+ { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal
+ { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULP(target);
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULP(${target}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface correctlyRoundedF32Case {
+ value: number;
+ expected: Array<number>;
+}
+
+g.test('correctlyRoundedF32')
+ .paramsSubcasesOnly<correctlyRoundedF32Case>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f32.infinity.positive, expected: [kValue.f32.positive.max, Number.POSITIVE_INFINITY] },
+ { value: kValue.f32.infinity.negative, expected: [Number.NEGATIVE_INFINITY, kValue.f32.negative.min] },
+ { value: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.subnormal.positive.min, expected: [kValue.f32.subnormal.positive.min] },
+ { value: kValue.f32.subnormal.positive.max, expected: [kValue.f32.subnormal.positive.max] },
+ { value: kValue.f32.subnormal.negative.min, expected: [kValue.f32.subnormal.negative.min] },
+ { value: kValue.f32.subnormal.negative.max, expected: [kValue.f32.subnormal.negative.max] },
+
+ // 64-bit subnormals
+ { value: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x00000000, 0x00000002), expected: [0, kValue.f32.subnormal.positive.min] },
+ { value: hexToF64(0x800fffff, 0xffffffff), expected: [kValue.f32.subnormal.negative.max, 0] },
+ { value: hexToF64(0x800fffff, 0xfffffffe), expected: [kValue.f32.subnormal.negative.max, 0] },
+
+ // 32-bit normals
+ { value: 0, expected: [0] },
+ { value: kValue.f32.positive.min, expected: [kValue.f32.positive.min] },
+ { value: kValue.f32.negative.max, expected: [kValue.f32.negative.max] },
+ { value: hexToF32(0x03800000), expected: [hexToF32(0x03800000)] },
+ { value: hexToF32(0x03800001), expected: [hexToF32(0x03800001)] },
+ { value: hexToF32(0x83800000), expected: [hexToF32(0x83800000)] },
+ { value: hexToF32(0x83800001), expected: [hexToF32(0x83800001)] },
+
+ // 64-bit normals
+ { value: hexToF64(0x3ff00000, 0x00000001), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] },
+ { value: hexToF64(0x3ff00000, 0x00000002), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] },
+ { value: hexToF64(0x3ff00010, 0x00000010), expected: [hexToF32(0x3f800080), hexToF32(0x3f800081)] },
+ { value: hexToF64(0x3ff00020, 0x00000020), expected: [hexToF32(0x3f800100), hexToF32(0x3f800101)] },
+ { value: hexToF64(0xbff00000, 0x00000001), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] },
+ { value: hexToF64(0xbff00000, 0x00000002), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] },
+ { value: hexToF64(0xbff00010, 0x00000010), expected: [hexToF32(0xbf800081), hexToF32(0xbf800080)] },
+ { value: hexToF64(0xbff00020, 0x00000020), expected: [hexToF32(0xbf800101), hexToF32(0xbf800100)] },
+ ]
+ )
+ .fn(t => {
+ const value = t.params.value;
+ const expected = t.params.expected;
+
+ const got = correctlyRoundedF32(value);
+ t.expect(
+ objectEquals(expected, got),
+ `correctlyRoundedF32(${value}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+interface lerpCase {
+ a: number;
+ b: number;
+ t: number;
+ result: number;
+}
+
+g.test('lerp')
+ .paramsSimple<lerpCase>([
+ // Infinite cases
+ { a: 0.0, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, t: 0.5, result: Number.NaN },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, t: 0.5, result: Number.NaN },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: 0.0, b: 1.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN },
+ { a: 1.0, b: 0.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN },
+ { a: 0.0, b: 1.0, t: Number.POSITIVE_INFINITY, result: Number.NaN },
+ { a: 1.0, b: 0.0, t: Number.POSITIVE_INFINITY, result: Number.NaN },
+
+ // [0.0, 1.0] cases
+ { a: 0.0, b: 1.0, t: -1.0, result: -1.0 },
+ { a: 0.0, b: 1.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: 1.0, t: 0.1, result: 0.1 },
+ { a: 0.0, b: 1.0, t: 0.01, result: 0.01 },
+ { a: 0.0, b: 1.0, t: 0.001, result: 0.001 },
+ { a: 0.0, b: 1.0, t: 0.25, result: 0.25 },
+ { a: 0.0, b: 1.0, t: 0.5, result: 0.5 },
+ { a: 0.0, b: 1.0, t: 0.9, result: 0.9 },
+ { a: 0.0, b: 1.0, t: 0.99, result: 0.99 },
+ { a: 0.0, b: 1.0, t: 0.999, result: 0.999 },
+ { a: 0.0, b: 1.0, t: 1.0, result: 1.0 },
+ { a: 0.0, b: 1.0, t: 2.0, result: 2.0 },
+
+ // [1.0, 0.0] cases
+ { a: 1.0, b: 0.0, t: -1.0, result: 2.0 },
+ { a: 1.0, b: 0.0, t: 0.0, result: 1.0 },
+ { a: 1.0, b: 0.0, t: 0.1, result: 0.9 },
+ { a: 1.0, b: 0.0, t: 0.01, result: 0.99 },
+ { a: 1.0, b: 0.0, t: 0.001, result: 0.999 },
+ { a: 1.0, b: 0.0, t: 0.25, result: 0.75 },
+ { a: 1.0, b: 0.0, t: 0.5, result: 0.5 },
+ { a: 1.0, b: 0.0, t: 0.9, result: 0.1 },
+ { a: 1.0, b: 0.0, t: 0.99, result: 0.01 },
+ { a: 1.0, b: 0.0, t: 0.999, result: 0.001 },
+ { a: 1.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: 1.0, b: 0.0, t: 2.0, result: -1.0 },
+
+ // [0.0, 10.0] cases
+ { a: 0.0, b: 10.0, t: -1.0, result: -10.0 },
+ { a: 0.0, b: 10.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: 10.0, t: 0.1, result: 1.0 },
+ { a: 0.0, b: 10.0, t: 0.01, result: 0.1 },
+ { a: 0.0, b: 10.0, t: 0.001, result: 0.01 },
+ { a: 0.0, b: 10.0, t: 0.25, result: 2.5 },
+ { a: 0.0, b: 10.0, t: 0.5, result: 5.0 },
+ { a: 0.0, b: 10.0, t: 0.9, result: 9.0 },
+ { a: 0.0, b: 10.0, t: 0.99, result: 9.9 },
+ { a: 0.0, b: 10.0, t: 0.999, result: 9.99 },
+ { a: 0.0, b: 10.0, t: 1.0, result: 10.0 },
+ { a: 0.0, b: 10.0, t: 2.0, result: 20.0 },
+
+ // [10.0, 0.0] cases
+ { a: 10.0, b: 0.0, t: -1.0, result: 20.0 },
+ { a: 10.0, b: 0.0, t: 0.0, result: 10.0 },
+ { a: 10.0, b: 0.0, t: 0.1, result: 9 },
+ { a: 10.0, b: 0.0, t: 0.01, result: 9.9 },
+ { a: 10.0, b: 0.0, t: 0.001, result: 9.99 },
+ { a: 10.0, b: 0.0, t: 0.25, result: 7.5 },
+ { a: 10.0, b: 0.0, t: 0.5, result: 5.0 },
+ { a: 10.0, b: 0.0, t: 0.9, result: 1.0 },
+ { a: 10.0, b: 0.0, t: 0.99, result: 0.1 },
+ { a: 10.0, b: 0.0, t: 0.999, result: 0.01 },
+ { a: 10.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: 10.0, b: 0.0, t: 2.0, result: -10.0 },
+
+ // [2.0, 10.0] cases
+ { a: 2.0, b: 10.0, t: -1.0, result: -6.0 },
+ { a: 2.0, b: 10.0, t: 0.0, result: 2.0 },
+ { a: 2.0, b: 10.0, t: 0.1, result: 2.8 },
+ { a: 2.0, b: 10.0, t: 0.01, result: 2.08 },
+ { a: 2.0, b: 10.0, t: 0.001, result: 2.008 },
+ { a: 2.0, b: 10.0, t: 0.25, result: 4.0 },
+ { a: 2.0, b: 10.0, t: 0.5, result: 6.0 },
+ { a: 2.0, b: 10.0, t: 0.9, result: 9.2 },
+ { a: 2.0, b: 10.0, t: 0.99, result: 9.92 },
+ { a: 2.0, b: 10.0, t: 0.999, result: 9.992 },
+ { a: 2.0, b: 10.0, t: 1.0, result: 10.0 },
+ { a: 2.0, b: 10.0, t: 2.0, result: 18.0 },
+
+ // [10.0, 2.0] cases
+ { a: 10.0, b: 2.0, t: -1.0, result: 18.0 },
+ { a: 10.0, b: 2.0, t: 0.0, result: 10.0 },
+ { a: 10.0, b: 2.0, t: 0.1, result: 9.2 },
+ { a: 10.0, b: 2.0, t: 0.01, result: 9.92 },
+ { a: 10.0, b: 2.0, t: 0.001, result: 9.992 },
+ { a: 10.0, b: 2.0, t: 0.25, result: 8.0 },
+ { a: 10.0, b: 2.0, t: 0.5, result: 6.0 },
+ { a: 10.0, b: 2.0, t: 0.9, result: 2.8 },
+ { a: 10.0, b: 2.0, t: 0.99, result: 2.08 },
+ { a: 10.0, b: 2.0, t: 0.999, result: 2.008 },
+ { a: 10.0, b: 2.0, t: 1.0, result: 2.0 },
+ { a: 10.0, b: 2.0, t: 2.0, result: -6.0 },
+
+ // [-1.0, 1.0] cases
+ { a: -1.0, b: 1.0, t: -2.0, result: -5.0 },
+ { a: -1.0, b: 1.0, t: 0.0, result: -1.0 },
+ { a: -1.0, b: 1.0, t: 0.1, result: -0.8 },
+ { a: -1.0, b: 1.0, t: 0.01, result: -0.98 },
+ { a: -1.0, b: 1.0, t: 0.001, result: -0.998 },
+ { a: -1.0, b: 1.0, t: 0.25, result: -0.5 },
+ { a: -1.0, b: 1.0, t: 0.5, result: 0.0 },
+ { a: -1.0, b: 1.0, t: 0.9, result: 0.8 },
+ { a: -1.0, b: 1.0, t: 0.99, result: 0.98 },
+ { a: -1.0, b: 1.0, t: 0.999, result: 0.998 },
+ { a: -1.0, b: 1.0, t: 1.0, result: 1.0 },
+ { a: -1.0, b: 1.0, t: 2.0, result: 3.0 },
+
+ // [1.0, -1.0] cases
+ { a: 1.0, b: -1.0, t: -2.0, result: 5.0 },
+ { a: 1.0, b: -1.0, t: 0.0, result: 1.0 },
+ { a: 1.0, b: -1.0, t: 0.1, result: 0.8 },
+ { a: 1.0, b: -1.0, t: 0.01, result: 0.98 },
+ { a: 1.0, b: -1.0, t: 0.001, result: 0.998 },
+ { a: 1.0, b: -1.0, t: 0.25, result: 0.5 },
+ { a: 1.0, b: -1.0, t: 0.5, result: 0.0 },
+ { a: 1.0, b: -1.0, t: 0.9, result: -0.8 },
+ { a: 1.0, b: -1.0, t: 0.99, result: -0.98 },
+ { a: 1.0, b: -1.0, t: 0.999, result: -0.998 },
+ { a: 1.0, b: -1.0, t: 1.0, result: -1.0 },
+ { a: 1.0, b: -1.0, t: 2.0, result: -3.0 },
+
+ // [-1.0, 0.0] cases
+ { a: -1.0, b: 0.0, t: -1.0, result: -2.0 },
+ { a: -1.0, b: 0.0, t: 0.0, result: -1.0 },
+ { a: -1.0, b: 0.0, t: 0.1, result: -0.9 },
+ { a: -1.0, b: 0.0, t: 0.01, result: -0.99 },
+ { a: -1.0, b: 0.0, t: 0.001, result: -0.999 },
+ { a: -1.0, b: 0.0, t: 0.25, result: -0.75 },
+ { a: -1.0, b: 0.0, t: 0.5, result: -0.5 },
+ { a: -1.0, b: 0.0, t: 0.9, result: -0.1 },
+ { a: -1.0, b: 0.0, t: 0.99, result: -0.01 },
+ { a: -1.0, b: 0.0, t: 0.999, result: -0.001 },
+ { a: -1.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: -1.0, b: 0.0, t: 2.0, result: 1.0 },
+
+ // [0.0, -1.0] cases
+ { a: 0.0, b: -1.0, t: -1.0, result: 1.0 },
+ { a: 0.0, b: -1.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: -1.0, t: 0.1, result: -0.1 },
+ { a: 0.0, b: -1.0, t: 0.01, result: -0.01 },
+ { a: 0.0, b: -1.0, t: 0.001, result: -0.001 },
+ { a: 0.0, b: -1.0, t: 0.25, result: -0.25 },
+ { a: 0.0, b: -1.0, t: 0.5, result: -0.5 },
+ { a: 0.0, b: -1.0, t: 0.9, result: -0.9 },
+ { a: 0.0, b: -1.0, t: 0.99, result: -0.99 },
+ { a: 0.0, b: -1.0, t: 0.999, result: -0.999 },
+ { a: 0.0, b: -1.0, t: 1.0, result: -1.0 },
+ { a: 0.0, b: -1.0, t: 2.0, result: -2.0 },
+ ])
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const t = test.params.t;
+ const got = lerp(a, b, t);
+ const expect = test.params.result;
+
+ test.expect(
+ (Number.isNaN(got) && Number.isNaN(expect)) || withinOneULP(got, expect, 'flush'),
+ `lerp(${a}, ${b}, ${t}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface rangeCase {
+ a: number;
+ b: number;
+ num_steps: number;
+ result: Array<number>;
+}
+
+g.test('linearRange')
+ .paramsSimple<rangeCase>(
+ // prettier-ignore
+ [
+ { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) },
+ { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) },
+ { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] },
+ { a: 10.0, b: 0.0, num_steps: 1, result: [10] },
+ { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] },
+ { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] },
+ { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0] },
+ { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 900.0, 800.0, 700.0, 600.0, 500.0, 400.0, 300.0, 200.0, 100.0, 0.0] },
+ { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 2.0, 3.0, 4.0, 5.0] },
+ { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.0, 3.0, 2.0, 1.0] },
+ { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0] },
+ { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.25, 0.5, 0.75, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.75, 0.5, 0.25, 0.0] },
+ { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0] },
+ { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, -0.2, -0.4, -0.6, -0.8, -1.0] },
+ { a: -1.0, b: 0, num_steps: 11, result: [-1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0] },
+ { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8, -0.9, -1.0] },
+ ]
+ )
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const num_steps = test.params.num_steps;
+ const got = linearRange(a, b, num_steps);
+ const expect = test.params.result;
+
+ test.expect(
+ compareArrayOfNumbers(got, expect, 'no-flush'),
+ `linearRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('biasedRange')
+ .paramsSimple<rangeCase>(
+ // prettier-ignore
+ [
+ { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) },
+ { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) },
+ { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] },
+ { a: 10.0, b: 0.0, num_steps: 1, result: [10.0] },
+ { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 0.1, 0.4, 0.9, 1.6, 2.5, 3.6, 4.9, 6.4, 8.1, 10.0] },
+ { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.9, 9.6, 9.1, 8.4, 7.5, 6.4, 5.1, 3.6, 1.9, 0.0] },
+ { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 10.0, 40.0, 90.0, 160.0, 250.0, 360.0, 490.0, 640.0, 810.0, 1000.0] },
+ { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 990.0, 960.0, 910.0, 840.0, 750.0, 640.0, 510.0, 360.0, 190.0, 0.0] },
+ { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 1.25, 2.0, 3.25, 5.0] },
+ { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.75, 4.0, 2.75, 1.0] },
+ { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.99, 0.96, 0.91, 0.84, 0.75, 0.64, 0.51, 0.36, 0.19, 0.0] },
+ { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.0625, 0.25, 0.5625, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.9375, 0.75, 0.4375, 0.0] },
+ { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.98, -0.92, -0.82, -0.68, -0.5, -0.28 ,-0.02, 0.28, 0.62, 1.0] },
+ { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.98, 0.92, 0.82, 0.68, 0.5, 0.28 ,0.02, -0.28, -0.62, -1.0] },
+ { a: -1.0, b: 0, num_steps: 11, result: [-1.0 , -0.99, -0.96, -0.91, -0.84, -0.75, -0.64, -0.51, -0.36, -0.19, 0.0] },
+ { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.01, -0.04, -0.09, -0.16, -0.25, -0.36, -0.49, -0.64, -0.81, -1.0] },
+ ]
+ )
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const num_steps = test.params.num_steps;
+ const got = biasedRange(a, b, num_steps);
+ const expect = test.params.result;
+
+ test.expect(
+ compareArrayOfNumbers(got, expect, 'no-flush'),
+ `biasedRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface fullF32RangeCase {
+ neg_norm: number;
+ neg_sub: number;
+ pos_sub: number;
+ pos_norm: number;
+ expect: Array<number>;
+}
+
+g.test('fullF32Range')
+ .paramsSimple<fullF32RangeCase>(
+ // prettier-ignore
+ [
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ 0.0 ] },
+ { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, 0.0] },
+ { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, 0.0 ] },
+ { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, -1.9999998807907104, kValue.f32.negative.max, 0.0 ] },
+ { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.subnormal.negative.min, 0.0 ] },
+ { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max, 0.0 ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ 0.0, kValue.f32.subnormal.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ 0.0, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ 0.0, kValue.f32.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ 0.0, kValue.f32.positive.min, kValue.f32.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ 0.0, kValue.f32.positive.min, 1.9999998807907104, kValue.f32.positive.max ] },
+ { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f32.negative.min, kValue.f32.subnormal.negative.min, 0.0, kValue.f32.subnormal.positive.min, kValue.f32.positive.min ] },
+ { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max, 0.0, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max, kValue.f32.positive.min, kValue.f32.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_norm = test.params.neg_norm;
+ const neg_sub = test.params.neg_sub;
+ const pos_sub = test.params.pos_sub;
+ const pos_norm = test.params.pos_norm;
+ const got = fullF32Range({ neg_norm, neg_sub, pos_sub, pos_norm });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbers(got, expect, 'no-flush'),
+ `fullF32Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface fullF16RangeCase {
+ neg_norm: number;
+ neg_sub: number;
+ pos_sub: number;
+ pos_norm: number;
+ expect: Array<number>;
+}
+
+g.test('fullF16Range')
+ .paramsSimple<fullF16RangeCase>(
+ // prettier-ignore
+ [
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ 0.0 ] },
+ { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, 0.0] },
+ { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, 0.0 ] },
+ { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, -1.9990234375, kValue.f16.negative.max, 0.0 ] },
+ { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.subnormal.negative.min, 0.0 ] },
+ { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.subnormal.negative.min, kValue.f16.subnormal.negative.max, 0.0 ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ 0.0, kValue.f16.subnormal.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ 0.0, kValue.f16.subnormal.positive.min, kValue.f16.subnormal.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ 0.0, kValue.f16.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ 0.0, kValue.f16.positive.min, kValue.f16.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ 0.0, kValue.f16.positive.min, 1.9990234375, kValue.f16.positive.max ] },
+ { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f16.negative.min, kValue.f16.subnormal.negative.min, 0.0, kValue.f16.subnormal.positive.min, kValue.f16.positive.min ] },
+ { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, kValue.f16.subnormal.negative.min, kValue.f16.subnormal.negative.max, 0.0, kValue.f16.subnormal.positive.min, kValue.f16.subnormal.positive.max, kValue.f16.positive.min, kValue.f16.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_norm = test.params.neg_norm;
+ const neg_sub = test.params.neg_sub;
+ const pos_sub = test.params.pos_sub;
+ const pos_norm = test.params.pos_norm;
+ const got = fullF16Range({ neg_norm, neg_sub, pos_sub, pos_norm });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbers(got, expect),
+ `fullF16Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface fullI32RangeCase {
+ neg_count: number;
+ pos_count: number;
+ expect: Array<number>;
+}
+
+g.test('fullI32Range')
+ .paramsSimple<fullI32RangeCase>(
+ // prettier-ignore
+ [
+ { neg_count: 0, pos_count: 0, expect: [0] },
+ { neg_count: 1, pos_count: 0, expect: [kValue.i32.negative.min, 0] },
+ { neg_count: 2, pos_count: 0, expect: [kValue.i32.negative.min, -1, 0] },
+ { neg_count: 3, pos_count: 0, expect: [kValue.i32.negative.min, -1610612736, -1, 0] },
+ { neg_count: 0, pos_count: 1, expect: [0, 1] },
+ { neg_count: 0, pos_count: 2, expect: [0, 1, kValue.i32.positive.max] },
+ { neg_count: 0, pos_count: 3, expect: [0, 1, 536870912, kValue.i32.positive.max] },
+ { neg_count: 1, pos_count: 1, expect: [kValue.i32.negative.min, 0, 1] },
+ { neg_count: 2, pos_count: 2, expect: [kValue.i32.negative.min, -1, 0, 1, kValue.i32.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_count = test.params.neg_count;
+ const pos_count = test.params.pos_count;
+ const got = fullI32Range({ negative: neg_count, positive: pos_count });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbers(got, expect),
+ `fullI32Range(${neg_count}, ${pos_count}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface limitsCase {
+ bits: number;
+ value: number;
+}
+
+// Test to confirm kBit and kValue constants are equivalent for f32
+g.test('f32LimitsEquivalency')
+ .paramsSimple<limitsCase>([
+ { bits: kBit.f32.positive.max, value: kValue.f32.positive.max },
+ { bits: kBit.f32.positive.min, value: kValue.f32.positive.min },
+ { bits: kBit.f32.positive.nearest_max, value: kValue.f32.positive.nearest_max },
+ { bits: kBit.f32.positive.less_than_one, value: kValue.f32.positive.less_than_one },
+ { bits: kBit.f32.positive.pi.whole, value: kValue.f32.positive.pi.whole },
+ { bits: kBit.f32.positive.pi.three_quarters, value: kValue.f32.positive.pi.three_quarters },
+ { bits: kBit.f32.positive.pi.half, value: kValue.f32.positive.pi.half },
+ { bits: kBit.f32.positive.pi.third, value: kValue.f32.positive.pi.third },
+ { bits: kBit.f32.positive.pi.quarter, value: kValue.f32.positive.pi.quarter },
+ { bits: kBit.f32.positive.pi.sixth, value: kValue.f32.positive.pi.sixth },
+ { bits: kBit.f32.positive.e, value: kValue.f32.positive.e },
+ { bits: kBit.f32.negative.max, value: kValue.f32.negative.max },
+ { bits: kBit.f32.negative.min, value: kValue.f32.negative.min },
+ { bits: kBit.f32.negative.nearest_min, value: kValue.f32.negative.nearest_min },
+ { bits: kBit.f32.negative.pi.whole, value: kValue.f32.negative.pi.whole },
+ { bits: kBit.f32.negative.pi.three_quarters, value: kValue.f32.negative.pi.three_quarters },
+ { bits: kBit.f32.negative.pi.half, value: kValue.f32.negative.pi.half },
+ { bits: kBit.f32.negative.pi.third, value: kValue.f32.negative.pi.third },
+ { bits: kBit.f32.negative.pi.quarter, value: kValue.f32.negative.pi.quarter },
+ { bits: kBit.f32.negative.pi.sixth, value: kValue.f32.negative.pi.sixth },
+ { bits: kBit.f32.subnormal.positive.max, value: kValue.f32.subnormal.positive.max },
+ { bits: kBit.f32.subnormal.positive.min, value: kValue.f32.subnormal.positive.min },
+ { bits: kBit.f32.subnormal.negative.max, value: kValue.f32.subnormal.negative.max },
+ { bits: kBit.f32.subnormal.negative.min, value: kValue.f32.subnormal.negative.min },
+ { bits: kBit.f32.infinity.positive, value: kValue.f32.infinity.positive },
+ { bits: kBit.f32.infinity.negative, value: kValue.f32.infinity.negative },
+ ])
+ .fn(test => {
+ const bits = test.params.bits;
+ const value = test.params.value;
+
+ const val_to_bits = bits === float32ToUint32(value);
+ const bits_to_val = value === uint32ToFloat32(bits);
+ test.expect(
+ val_to_bits && bits_to_val,
+ `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent`
+ );
+ });
+
+// Test to confirm kBit and kValue constants are equivalent for f16
+g.test('f16LimitsEquivalency')
+ .paramsSimple<limitsCase>([
+ { bits: kBit.f16.positive.max, value: kValue.f16.positive.max },
+ { bits: kBit.f16.positive.min, value: kValue.f16.positive.min },
+ { bits: kBit.f16.negative.max, value: kValue.f16.negative.max },
+ { bits: kBit.f16.negative.min, value: kValue.f16.negative.min },
+ { bits: kBit.f16.subnormal.positive.max, value: kValue.f16.subnormal.positive.max },
+ { bits: kBit.f16.subnormal.positive.min, value: kValue.f16.subnormal.positive.min },
+ { bits: kBit.f16.subnormal.negative.max, value: kValue.f16.subnormal.negative.max },
+ { bits: kBit.f16.subnormal.negative.min, value: kValue.f16.subnormal.negative.min },
+ { bits: kBit.f16.infinity.positive, value: kValue.f16.infinity.positive },
+ { bits: kBit.f16.infinity.negative, value: kValue.f16.infinity.negative },
+ ])
+ .fn(test => {
+ const bits = test.params.bits;
+ const value = test.params.value;
+
+ const val_to_bits = bits === float16ToUint16(value);
+ const bits_to_val = value === uint16ToFloat16(bits);
+ test.expect(
+ val_to_bits && bits_to_val,
+ `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent`
+ );
+ });
+
+interface cartesianProductCase<T> {
+ inputs: T[][];
+ result: T[][];
+}
+
+g.test('cartesianProductNumber')
+ .paramsSimple<cartesianProductCase<number>>(
+ // prettier-ignore
+ [
+ { inputs: [[0], [1]], result: [[0, 1]] },
+ { inputs: [[0, 1], [2]], result: [[0, 2],
+ [1, 2]] },
+ { inputs: [[0], [1, 2]], result: [[0, 1],
+ [0, 2]] },
+ { inputs: [[0, 1], [2, 3]], result: [[0,2],
+ [1, 2],
+ [0, 3],
+ [1, 3]] },
+ { inputs: [[0, 1, 2], [3, 4, 5]], result: [[0, 3],
+ [1, 3],
+ [2, 3],
+ [0, 4],
+ [1, 4],
+ [2, 4],
+ [0, 5],
+ [1, 5],
+ [2, 5]] },
+ { inputs: [[0, 1], [2, 3], [4, 5]], result: [[0, 2, 4],
+ [1, 2, 4],
+ [0, 3, 4],
+ [1, 3, 4],
+ [0, 2, 5],
+ [1, 2, 5],
+ [0, 3, 5],
+ [1, 3, 5]] },
+
+ ]
+ )
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = cartesianProduct(...inputs);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
+
+g.test('cartesianProductArray')
+ .paramsSimple<cartesianProductCase<number[]>>(
+ // prettier-ignore
+ [
+ { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]]], result: [[[0, 1], [4, 5]],
+ [[2, 3], [4, 5]],
+ [[0, 1], [6, 7]],
+ [[2, 3], [6, 7]]]},
+ { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]], [[8, 9]]], result: [[[0, 1], [4, 5], [8, 9]],
+ [[2, 3], [4, 5], [8, 9]],
+ [[0, 1], [6, 7], [8, 9]],
+ [[2, 3], [6, 7], [8, 9]]]},
+ { inputs: [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[2, 1, 0], [5, 4, 3], [8, 7, 6]]], result: [[[0, 1, 2], [2, 1, 0]],
+ [[3, 4, 5], [2, 1, 0]],
+ [[6, 7, 8], [2, 1, 0]],
+ [[0, 1, 2], [5, 4, 3]],
+ [[3, 4, 5], [5, 4, 3]],
+ [[6, 7, 8], [5, 4, 3]],
+ [[0, 1, 2], [8, 7, 6]],
+ [[3, 4, 5], [8, 7, 6]],
+ [[6, 7, 8], [8, 7, 6]]]}
+
+ ]
+ )
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = cartesianProduct(...inputs);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
+
+interface calculatePermutationsCase<T> {
+ input: T[];
+ result: T[][];
+}
+
+g.test('calculatePermutations')
+ .paramsSimple<calculatePermutationsCase<number>>(
+ // prettier-ignore
+ [
+ { input: [0, 1], result: [[0, 1],
+ [1, 0]] },
+ { input: [0, 1, 2], result: [[0, 1, 2],
+ [0, 2, 1],
+ [1, 0, 2],
+ [1, 2, 0],
+ [2, 0, 1],
+ [2, 1, 0]] },
+ { input: [0, 1, 2, 3], result: [[0, 1, 2, 3],
+ [0, 1, 3, 2],
+ [0, 2, 1, 3],
+ [0, 2, 3, 1],
+ [0, 3, 1, 2],
+ [0, 3, 2, 1],
+ [1, 0, 2, 3],
+ [1, 0, 3, 2],
+ [1, 2, 0, 3],
+ [1, 2, 3, 0],
+ [1, 3, 0, 2],
+ [1, 3, 2, 0],
+ [2, 0, 1, 3],
+ [2, 0, 3, 1],
+ [2, 1, 0, 3],
+ [2, 1, 3, 0],
+ [2, 3, 0, 1],
+ [2, 3, 1, 0],
+ [3, 0, 1, 2],
+ [3, 0, 2, 1],
+ [3, 1, 0, 2],
+ [3, 1, 2, 0],
+ [3, 2, 0, 1],
+ [3, 2, 1, 0]] },
+ ]
+ )
+ .fn(test => {
+ const input = test.params.input;
+ const got = calculatePermutations(input);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `calculatePermutations(${JSON.stringify(input)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts
new file mode 100644
index 0000000000..5c20af355d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts
@@ -0,0 +1,440 @@
+export const description = `
+Unit tests for parameterization helpers.
+`;
+
+import {
+ kUnitCaseParamsBuilder,
+ CaseSubcaseIterable,
+ ParamsBuilderBase,
+ builderIterateCasesWithSubcases,
+} from '../common/framework/params_builder.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { mergeParams, publicParamsEquals } from '../common/internal/params_utils.js';
+import { assert, objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+class ParamsTest extends UnitTest {
+ expectParams<CaseP, SubcaseP>(
+ act: ParamsBuilderBase<CaseP, SubcaseP>,
+ exp: CaseSubcaseIterable<{}, {}>
+ ): void {
+ const a = Array.from(builderIterateCasesWithSubcases(act)).map(([caseP, subcases]) => [
+ caseP,
+ subcases ? Array.from(subcases) : undefined,
+ ]);
+ const e = Array.from(exp);
+ this.expect(
+ objectEquals(a, e),
+ `
+got ${JSON.stringify(a)}
+expected ${JSON.stringify(e)}`
+ );
+ }
+}
+
+export const g = makeTestGroup(ParamsTest);
+
+const u = kUnitCaseParamsBuilder;
+
+g.test('combine').fn(t => {
+ t.expectParams<{ hello: number }, {}>(u.combine('hello', [1, 2, 3]), [
+ [{ hello: 1 }, undefined],
+ [{ hello: 2 }, undefined],
+ [{ hello: 3 }, undefined],
+ ]);
+ t.expectParams<{ hello: 1 | 2 | 3 }, {}>(u.combine('hello', [1, 2, 3] as const), [
+ [{ hello: 1 }, undefined],
+ [{ hello: 2 }, undefined],
+ [{ hello: 3 }, undefined],
+ ]);
+ t.expectParams<{}, { hello: number }>(u.beginSubcases().combine('hello', [1, 2, 3]), [
+ [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]],
+ ]);
+ t.expectParams<{}, { hello: 1 | 2 | 3 }>(u.beginSubcases().combine('hello', [1, 2, 3] as const), [
+ [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]],
+ ]);
+});
+
+g.test('empty').fn(t => {
+ t.expectParams<{}, {}>(u, [
+ [{}, undefined], //
+ ]);
+ t.expectParams<{}, {}>(u.beginSubcases(), [
+ [{}, [{}]], //
+ ]);
+});
+
+g.test('combine,zeroes_and_ones').fn(t => {
+ t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([{}]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([{}]), [
+ [{}, undefined], //
+ ]);
+
+ t.expectParams<{}, {}>(u.combine('x', []).combine('y', []), []);
+ t.expectParams<{}, {}>(u.combine('x', []).combine('y', [1]), []);
+ t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', []), []);
+ t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', [1]), [
+ [{ x: 1, y: 1 }, undefined], //
+ ]);
+});
+
+g.test('combine,mixed').fn(t => {
+ t.expectParams<{ x: number; y: string; p: number | undefined; q: number | undefined }, {}>(
+ u
+ .combine('x', [1, 2])
+ .combine('y', ['a', 'b'])
+ .combineWithParams([{ p: 4 }, { q: 5 }])
+ .combineWithParams([{}]),
+ [
+ [{ x: 1, y: 'a', p: 4 }, undefined],
+ [{ x: 1, y: 'a', q: 5 }, undefined],
+ [{ x: 1, y: 'b', p: 4 }, undefined],
+ [{ x: 1, y: 'b', q: 5 }, undefined],
+ [{ x: 2, y: 'a', p: 4 }, undefined],
+ [{ x: 2, y: 'a', q: 5 }, undefined],
+ [{ x: 2, y: 'b', p: 4 }, undefined],
+ [{ x: 2, y: 'b', q: 5 }, undefined],
+ ]
+ );
+});
+
+g.test('filter').fn(t => {
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a),
+ [
+ [{ a: true, x: 1 }, undefined], //
+ ]
+ );
+
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .filter(p => p.a),
+ [
+ [{ a: true, x: 1 }, [{}]], //
+ // Case with no subcases is filtered out.
+ ]
+ );
+
+ t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>(
+ u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a),
+ [
+ [{}, [{ a: true, x: 1 }]], //
+ ]
+ );
+});
+
+g.test('unless').fn(t => {
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a),
+ [
+ [{ a: false, y: 2 }, undefined], //
+ ]
+ );
+
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .unless(p => p.a),
+ [
+ // Case with no subcases is filtered out.
+ [{ a: false, y: 2 }, [{}]], //
+ ]
+ );
+
+ t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>(
+ u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a),
+ [
+ [{}, [{ a: false, y: 2 }]], //
+ ]
+ );
+});
+
+g.test('expandP').fn(t => {
+ // simple
+ t.expectParams<{}, {}>(
+ u.expandWithParams(function* () {}),
+ []
+ );
+ t.expectParams<{}, {}>(
+ u.expandWithParams(function* () {
+ yield {};
+ }),
+ [[{}, undefined]]
+ );
+ t.expectParams<{ z: number | undefined; w: number | undefined }, {}>(
+ u.expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ [{ w: 5 }, undefined],
+ ]
+ );
+ t.expectParams<{}, { z: number | undefined; w: number | undefined }>(
+ u.beginSubcases().expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [[{}, [{ z: 3 }, { z: 4 }, { w: 5 }]]]
+ );
+
+ // more complex
+ t.expectParams<
+ {
+ a: boolean;
+ x: number | undefined;
+ y: number | undefined;
+ z: number | undefined;
+ w: number | undefined;
+ },
+ {}
+ >(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { z: 3 };
+ yield { z: 4 };
+ } else {
+ yield { w: 5 };
+ }
+ }),
+ [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: true, x: 1, z: 4 }, undefined],
+ [{ a: false, y: 2, w: 5 }, undefined],
+ ]
+ );
+ t.expectParams<
+ { a: boolean; x: number | undefined; y: number | undefined },
+ { z: number | undefined; w: number | undefined }
+ >(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { z: 3 };
+ yield { z: 4 };
+ } else {
+ yield { w: 5 };
+ }
+ }),
+ [
+ [{ a: true, x: 1 }, [{ z: 3 }, { z: 4 }]],
+ [{ a: false, y: 2 }, [{ w: 5 }]],
+ ]
+ );
+});
+
+g.test('expand').fn(t => {
+ // simple
+ t.expectParams<{}, {}>(
+ u.expand('x', function* () {}),
+ []
+ );
+ t.expectParams<{ z: number }, {}>(
+ u.expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ ]
+ );
+ t.expectParams<{}, { z: number }>(
+ u.beginSubcases().expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [[{}, [{ z: 3 }, { z: 4 }]]]
+ );
+
+ // more complex
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined; z: number }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .expand('z', function* (p) {
+ if (p.a) {
+ yield 3;
+ } else {
+ yield 5;
+ }
+ }),
+ [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: false, y: 2, z: 5 }, undefined],
+ ]
+ );
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, { z: number }>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .expand('z', function* (p) {
+ if (p.a) {
+ yield 3;
+ } else {
+ yield 5;
+ }
+ }),
+ [
+ [{ a: true, x: 1 }, [{ z: 3 }]],
+ [{ a: false, y: 2 }, [{ z: 5 }]],
+ ]
+ );
+});
+
+g.test('invalid,shadowing').fn(t => {
+ // Existing CaseP is shadowed by a new CaseP.
+ {
+ const p = u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ // Iterating causes e.g. mergeParams({x:1}, {x:3}), which fails.
+ t.shouldThrow('Error', () => {
+ Array.from(p.iterateCasesWithSubcases());
+ });
+ }
+ // Existing SubcaseP is shadowed by a new SubcaseP.
+ {
+ const p = u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ // Iterating causes e.g. mergeParams({x:1}, {x:3}), which fails.
+ t.shouldThrow('Error', () => {
+ Array.from(p.iterateCasesWithSubcases());
+ });
+ }
+ // Existing CaseP is shadowed by a new SubcaseP.
+ {
+ const p = u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .beginSubcases()
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ const cases = Array.from(p.iterateCasesWithSubcases());
+ // Iterating cases is fine...
+ for (const [caseP, subcases] of cases) {
+ assert(subcases !== undefined);
+ // Iterating subcases is fine...
+ for (const subcaseP of subcases) {
+ if (caseP.a) {
+ assert(subcases !== undefined);
+ // Only errors once we try to e.g. mergeParams({x:1}, {x:3}).
+ t.shouldThrow('Error', () => {
+ mergeParams(caseP, subcaseP);
+ });
+ } else {
+ mergeParams(caseP, subcaseP);
+ }
+ }
+ }
+ }
+});
+
+g.test('undefined').fn(t => {
+ t.expect(!publicParamsEquals({ a: undefined }, {}));
+ t.expect(!publicParamsEquals({}, { a: undefined }));
+});
+
+g.test('private').fn(t => {
+ t.expect(publicParamsEquals({ _a: 0 }, {}));
+ t.expect(publicParamsEquals({}, { _a: 0 }));
+});
+
+g.test('value,array').fn(t => {
+ t.expectParams<{ a: number[] }, {}>(u.combineWithParams([{ a: [1, 2] }]), [
+ [{ a: [1, 2] }, undefined], //
+ ]);
+ t.expectParams<{}, { a: number[] }>(u.beginSubcases().combineWithParams([{ a: [1, 2] }]), [
+ [{}, [{ a: [1, 2] }]], //
+ ]);
+});
+
+g.test('value,object').fn(t => {
+ t.expectParams<{ a: { [k: string]: number } }, {}>(u.combineWithParams([{ a: { x: 1 } }]), [
+ [{ a: { x: 1 } }, undefined], //
+ ]);
+ t.expectParams<{}, { a: { [k: string]: number } }>(
+ u.beginSubcases().combineWithParams([{ a: { x: 1 } }]),
+ [
+ [{}, [{ a: { x: 1 } }]], //
+ ]
+ );
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts
new file mode 100644
index 0000000000..08a84b23e7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts
@@ -0,0 +1,112 @@
+export const description = `
+Unit tests for parameterization.
+`;
+
+import { TestParams } from '../common/framework/fixture.js';
+import { kUnitCaseParamsBuilder } from '../common/framework/params_builder.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('combine_none,arg_unit')
+ .params(u => u.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,arg_ignored')
+ .params(() => kUnitCaseParamsBuilder.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,plain_builder')
+ .params(kUnitCaseParamsBuilder.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,plain_array')
+ .paramsSimple([])
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_one,case')
+ .params(u =>
+ u //
+ .combineWithParams([{ x: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.x === 1);
+ });
+
+g.test('combine_one,subcase')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([{ x: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.x === 1);
+ });
+
+g.test('filter')
+ .params(u =>
+ u
+ .combineWithParams([
+ { a: true, x: 1 }, //
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a)
+ )
+ .fn(t => {
+ t.expect(t.params.a);
+ });
+
+g.test('unless')
+ .params(u =>
+ u
+ .combineWithParams([
+ { a: true, x: 1 }, //
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a)
+ )
+ .fn(t => {
+ t.expect(!t.params.a);
+ });
+
+g.test('generator').fn(t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const ran: TestParams[] = [];
+
+ g.test('generator')
+ .params(u =>
+ u.combineWithParams({
+ *[Symbol.iterator]() {
+ for (let x = 0; x < 3; ++x) {
+ for (let y = 0; y < 2; ++y) {
+ yield { x, y };
+ }
+ }
+ },
+ })
+ )
+ .fn(t => {
+ ran.push(t.params);
+ });
+
+ t0.expectCases(g, [
+ { test: ['generator'], params: { x: 0, y: 0 } },
+ { test: ['generator'], params: { x: 0, y: 1 } },
+ { test: ['generator'], params: { x: 1, y: 0 } },
+ { test: ['generator'], params: { x: 1, y: 1 } },
+ { test: ['generator'], params: { x: 2, y: 0 } },
+ { test: ['generator'], params: { x: 2, y: 1 } },
+ ]);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts
new file mode 100644
index 0000000000..040629355d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts
@@ -0,0 +1,207 @@
+export const description = `
+Test for "pp" preprocessor.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { pp } from '../common/util/preprocessor.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ test(act: string, exp: string): void {
+ this.expect(act === exp, 'got: ' + act.replace('\n', '⏎'));
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('empty').fn(t => {
+ t.test(pp``, '');
+ t.test(pp`\n`, '\n');
+ t.test(pp`\n\n`, '\n\n');
+});
+
+g.test('plain').fn(t => {
+ t.test(pp`a`, 'a');
+ t.test(pp`\na`, '\na');
+ t.test(pp`\n\na`, '\n\na');
+ t.test(pp`\na\n`, '\na\n');
+ t.test(pp`a\n\n`, 'a\n\n');
+});
+
+g.test('substitutions,1').fn(t => {
+ const act = pp`a ${3} b`;
+ const exp = 'a 3 b';
+ t.test(act, exp);
+});
+
+g.test('substitutions,2').fn(t => {
+ const act = pp`a ${'x'}`;
+ const exp = 'a x';
+ t.test(act, exp);
+});
+
+g.test('substitutions,3').fn(t => {
+ const act = pp`a ${'x'} b`;
+ const exp = 'a x b';
+ t.test(act, exp);
+});
+
+g.test('substitutions,4').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+${'x'}
+${pp._endif}
+b`;
+ const exp = '\na\n\nb';
+ t.test(act, exp);
+});
+
+g.test('if,true').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}c${pp._endif}
+d
+`;
+ const exp = '\na\nc\nd\n';
+ t.test(act, exp);
+});
+
+g.test('if,false').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}c${pp._endif}
+d
+`;
+ const exp = '\na\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('else,1').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}
+b
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nb\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('else,2').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nc\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('elif,1').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._elif(true)}
+e
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\ne\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('elif,2').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}
+b
+${pp._elif(true)}
+e
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nb\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('nested,1').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp.__if(true)}
+e
+${pp.__endif}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('nested,2').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._else}
+h
+${pp.__if(false)}
+e
+${pp.__elif(true)}
+f
+${pp.__else}
+g
+${pp.__endif}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nh\n\nf\n\nc\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('errors,pass').fn(() => {
+ pp`${pp._if(true)}${pp._endif}`;
+ pp`${pp._if(true)}${pp._else}${pp._endif}`;
+ pp`${pp._if(true)}${pp.__if(true)}${pp.__endif}${pp._endif}`;
+});
+
+g.test('errors,fail').fn(t => {
+ const e = (fn: () => void) => t.shouldThrow('Error', fn);
+ e(() => pp`${pp._if(true)}`);
+ e(() => pp`${pp._elif(true)}`);
+ e(() => pp`${pp._else}`);
+ e(() => pp`${pp._endif}`);
+ e(() => pp`${pp.__if(true)}`);
+ e(() => pp`${pp.__elif(true)}`);
+ e(() => pp`${pp.__else}`);
+ e(() => pp`${pp.__endif}`);
+
+ e(() => pp`${pp._if(true)}${pp._elif(true)}`);
+ e(() => pp`${pp._if(true)}${pp._elif(true)}${pp._else}`);
+ e(() => pp`${pp._if(true)}${pp._else}`);
+ e(() => pp`${pp._else}${pp._endif}`);
+
+ e(() => pp`${pp._if(true)}${pp.__endif}`);
+ e(() => pp`${pp.__if(true)}${pp.__endif}`);
+ e(() => pp`${pp.__if(true)}${pp._endif}`);
+
+ e(() => pp`${pp._if(true)}${pp._else}${pp._else}${pp._endif}`);
+ e(() => pp`${pp._if(true)}${pp.__if(true)}${pp.__else}${pp.__else}${pp.__endif}${pp._endif}`);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts
new file mode 100644
index 0000000000..520af9e663
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts
@@ -0,0 +1,133 @@
+export const description = `
+Tests for TestQuery comparison
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { compareQueries, Ordering } from '../common/internal/query/compare.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+ TestQueryMultiCase,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ expectQ(a: TestQuery, exp: '<' | '=' | '>' | '!', b: TestQuery) {
+ const [expOrdering, expInvOrdering] =
+ exp === '<'
+ ? [Ordering.StrictSubset, Ordering.StrictSuperset]
+ : exp === '='
+ ? [Ordering.Equal, Ordering.Equal]
+ : exp === '>'
+ ? [Ordering.StrictSuperset, Ordering.StrictSubset]
+ : [Ordering.Unordered, Ordering.Unordered];
+ {
+ const act = compareQueries(a, b);
+ this.expect(act === expOrdering, `${a} ${b} got ${act}, exp ${expOrdering}`);
+ }
+ {
+ const act = compareQueries(a, b);
+ this.expect(act === expOrdering, `${b} ${a} got ${act}, exp ${expInvOrdering}`);
+ }
+ }
+
+ expectWellOrdered(...qs: TestQuery[]) {
+ for (let i = 0; i < qs.length; ++i) {
+ this.expectQ(qs[i], '=', qs[i]);
+ for (let j = i + 1; j < qs.length; ++j) {
+ this.expectQ(qs[i], '>', qs[j]);
+ }
+ }
+ }
+
+ expectUnordered(...qs: TestQuery[]) {
+ for (let i = 0; i < qs.length; ++i) {
+ this.expectQ(qs[i], '=', qs[i]);
+ for (let j = i + 1; j < qs.length; ++j) {
+ this.expectQ(qs[i], '!', qs[j]);
+ }
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+// suite:* > suite:a,* > suite:a,b,* > suite:a,b:*
+// suite:a,b:* > suite:a,b:c,* > suite:a,b:c,d,* > suite:a,b:c,d:*
+// suite:a,b:c,d:* > suite:a,b:c,d:x=1;* > suite:a,b:c,d:x=1;y=2;* > suite:a,b:c,d:x=1;y=2
+// suite:a;* (unordered) suite:b;*
+g.test('well_ordered').fn(t => {
+ t.expectWellOrdered(
+ new TestQueryMultiFile('suite', []),
+ new TestQueryMultiFile('suite', ['a']),
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiTest('suite', ['a', 'b'], []),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectWellOrdered(
+ new TestQueryMultiFile('suite', []),
+ new TestQueryMultiFile('suite', ['a']),
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiTest('suite', ['a', 'b'], []),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], {})
+ );
+});
+
+g.test('unordered').fn(t => {
+ t.expectUnordered(
+ new TestQueryMultiFile('suite', ['a']), //
+ new TestQueryMultiFile('suite', ['x'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiFile('suite', ['a', 'x'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], [])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], [])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], ['c'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 9 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 9 })
+ );
+ t.expectUnordered(
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 })
+ );
+ t.expectUnordered(
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 })
+ );
+ t.expectUnordered(
+ new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}),
+ new TestQueryMultiTest('suite1', ['bar'], [])
+ );
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts
new file mode 100644
index 0000000000..040acd1b87
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts
@@ -0,0 +1,268 @@
+export const description = `
+Unit tests for TestQuery strings.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { compareQueries, Ordering } from '../common/internal/query/compare.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiCase,
+ TestQueryMultiTest,
+ TestQueryMultiFile,
+ relativeQueryString,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class T extends UnitTest {
+ expectQueryString(q: TestQuery, exp: string): void {
+ const s = q.toString();
+ this.expect(s === exp, `got ${s} expected ${exp}`);
+ }
+
+ expectRelativeQueryString(parent: TestQuery, child: TestQuery, exp: string): void {
+ const s = relativeQueryString(parent, child);
+ this.expect(s === exp, `got ${s} expected ${exp}`);
+
+ if (compareQueries(parent, child) !== Ordering.Equal) {
+ // Test in reverse
+ this.shouldThrow('Error', () => {
+ relativeQueryString(child, parent);
+ });
+ }
+ }
+}
+
+export const g = makeTestGroup(T);
+
+g.test('stringifyQuery,single_case').fn(t => {
+ t.expectQueryString(
+ new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ _pri1: 0,
+ x: 3,
+ _pri2: 1,
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";x=3'
+ );
+});
+
+g.test('stringifyQuery,single_case,json').fn(t => {
+ t.expectQueryString(
+ new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ x: { p: 2, q: 'Q' },
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";x={"p":2,"q":"Q"}'
+ );
+});
+
+g.test('stringifyQuery,multi_case').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ _pri1: 0,
+ a: 3,
+ _pri2: 1,
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";a=3;*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {}),
+ 'a:b_1,2_c:d_3,4_e:*'
+ );
+});
+
+g.test('stringifyQuery,multi_test').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiTest('a', ['b_1', '2_c'], ['d_3', '4_e']),
+ 'a:b_1,2_c:d_3,4_e,*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiTest('a', ['b_1', '2_c'], []), //
+ 'a:b_1,2_c:*'
+ );
+});
+
+g.test('stringifyQuery,multi_file').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiFile('a', ['b_1', '2_c']), //
+ 'a:b_1,2_c,*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiFile('a', []), //
+ 'a:*'
+ );
+});
+
+g.test('relativeQueryString,equal_or_child').fn(t => {
+ // Depth difference = 0
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', []), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ''
+ );
+
+ // Depth difference = 1
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', ['b']), //
+ ':b,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ',c,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ ':*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ',e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ ':*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ':f=0;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ';g=1;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ''
+ );
+
+ // Depth difference = 2
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ':b,c,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ ',e:*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ':f=0;g=1;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1, h: 2 }), //
+ ';h=2'
+ );
+ // Depth difference = 2
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ ',c:*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ':d,e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ':f=0;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ';g=1'
+ );
+
+ // Depth difference = 4
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':b,c:d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ',e:f=0;g=1;*'
+ );
+ // Depth difference = 4
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ',c:d,e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ':f=0;g=1'
+ );
+});
+
+g.test('relativeQueryString,unrelated').fn(t => {
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'x']), //
+ new TestQueryMultiFile('a', ['b', 'c']) //
+ );
+ });
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'x']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']) //
+ );
+ });
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 1 }) //
+ );
+ });
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts
new file mode 100644
index 0000000000..73bfcbef9f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts
@@ -0,0 +1,259 @@
+export const description = `Unit tests for data cache serialization`;
+
+import { getIsBuildingDataCache, setIsBuildingDataCache } from '../common/framework/data_cache.js';
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import {
+ deserializeExpectation,
+ serializeExpectation,
+} from '../webgpu/shader/execution/expression/case_cache.js';
+import {
+ anyOf,
+ deserializeComparator,
+ SerializedComparator,
+ skipUndefined,
+} from '../webgpu/util/compare.js';
+import { kValue } from '../webgpu/util/constants.js';
+import {
+ bool,
+ deserializeValue,
+ f16,
+ f32,
+ i16,
+ i32,
+ i8,
+ serializeValue,
+ u16,
+ u32,
+ u8,
+ vec2,
+ vec3,
+ vec4,
+} from '../webgpu/util/conversion.js';
+import {
+ deserializeF32Interval,
+ serializeF32Interval,
+ toF32Interval,
+} from '../webgpu/util/f32_interval.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('value').fn(t => {
+ for (const value of [
+ u32(kValue.u32.min + 0),
+ u32(kValue.u32.min + 1),
+ u32(kValue.u32.min + 2),
+ u32(kValue.u32.max - 2),
+ u32(kValue.u32.max - 1),
+ u32(kValue.u32.max - 0),
+
+ u16(kValue.u16.min + 0),
+ u16(kValue.u16.min + 1),
+ u16(kValue.u16.min + 2),
+ u16(kValue.u16.max - 2),
+ u16(kValue.u16.max - 1),
+ u16(kValue.u16.max - 0),
+
+ u8(kValue.u8.min + 0),
+ u8(kValue.u8.min + 1),
+ u8(kValue.u8.min + 2),
+ u8(kValue.u8.max - 2),
+ u8(kValue.u8.max - 1),
+ u8(kValue.u8.max - 0),
+
+ i32(kValue.i32.negative.min + 0),
+ i32(kValue.i32.negative.min + 1),
+ i32(kValue.i32.negative.min + 2),
+ i32(kValue.i32.negative.max - 2),
+ i32(kValue.i32.negative.max - 1),
+ i32(kValue.i32.positive.min - 0),
+ i32(kValue.i32.positive.min + 1),
+ i32(kValue.i32.positive.min + 2),
+ i32(kValue.i32.positive.max - 2),
+ i32(kValue.i32.positive.max - 1),
+ i32(kValue.i32.positive.max - 0),
+
+ i16(kValue.i16.negative.min + 0),
+ i16(kValue.i16.negative.min + 1),
+ i16(kValue.i16.negative.min + 2),
+ i16(kValue.i16.negative.max - 2),
+ i16(kValue.i16.negative.max - 1),
+ i16(kValue.i16.positive.min + 0),
+ i16(kValue.i16.positive.min + 1),
+ i16(kValue.i16.positive.min + 2),
+ i16(kValue.i16.positive.max - 2),
+ i16(kValue.i16.positive.max - 1),
+ i16(kValue.i16.positive.max - 0),
+
+ i8(kValue.i8.negative.min + 0),
+ i8(kValue.i8.negative.min + 1),
+ i8(kValue.i8.negative.min + 2),
+ i8(kValue.i8.negative.max - 2),
+ i8(kValue.i8.negative.max - 1),
+ i8(kValue.i8.positive.min + 0),
+ i8(kValue.i8.positive.min + 1),
+ i8(kValue.i8.positive.min + 2),
+ i8(kValue.i8.positive.max - 2),
+ i8(kValue.i8.positive.max - 1),
+ i8(kValue.i8.positive.max - 0),
+
+ f32(0),
+ f32(-0),
+ f32(1),
+ f32(-1),
+ f32(0.5),
+ f32(-0.5),
+ f32(kValue.f32.positive.max),
+ f32(kValue.f32.positive.min),
+ f32(kValue.f32.subnormal.positive.max),
+ f32(kValue.f32.subnormal.positive.min),
+ f32(kValue.f32.subnormal.negative.max),
+ f32(kValue.f32.subnormal.negative.min),
+ f32(kValue.f32.infinity.positive),
+ f32(kValue.f32.infinity.negative),
+
+ f16(0),
+ f16(-0),
+ f16(1),
+ f16(-1),
+ f16(0.5),
+ f16(-0.5),
+ f16(kValue.f32.positive.max),
+ f16(kValue.f32.positive.min),
+ f16(kValue.f32.subnormal.positive.max),
+ f16(kValue.f32.subnormal.positive.min),
+ f16(kValue.f32.subnormal.negative.max),
+ f16(kValue.f32.subnormal.negative.min),
+ f16(kValue.f32.infinity.positive),
+ f16(kValue.f32.infinity.negative),
+
+ bool(true),
+ bool(false),
+
+ vec2(f32(1), f32(2)),
+ vec3(u32(1), u32(2), u32(3)),
+ vec4(bool(false), bool(true), bool(false), bool(true)),
+ ]) {
+ const serialized = serializeValue(value);
+ const deserialized = deserializeValue(serialized);
+ t.expect(
+ objectEquals(value, deserialized),
+ `value ${value} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+g.test('f32_interval').fn(t => {
+ for (const interval of [
+ toF32Interval(0),
+ toF32Interval(-0),
+ toF32Interval(1),
+ toF32Interval(-1),
+ toF32Interval(0.5),
+ toF32Interval(-0.5),
+ toF32Interval(kValue.f32.positive.max),
+ toF32Interval(kValue.f32.positive.min),
+ toF32Interval(kValue.f32.subnormal.positive.max),
+ toF32Interval(kValue.f32.subnormal.positive.min),
+ toF32Interval(kValue.f32.subnormal.negative.max),
+ toF32Interval(kValue.f32.subnormal.negative.min),
+ toF32Interval(kValue.f32.infinity.positive),
+ toF32Interval(kValue.f32.infinity.negative),
+
+ toF32Interval([-0, 0]),
+ toF32Interval([-1, 1]),
+ toF32Interval([-0.5, 0.5]),
+ toF32Interval([kValue.f32.positive.min, kValue.f32.positive.max]),
+ toF32Interval([kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max]),
+ toF32Interval([kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max]),
+ toF32Interval([kValue.f32.infinity.negative, kValue.f32.infinity.positive]),
+ ]) {
+ const serialized = serializeF32Interval(interval);
+ const deserialized = deserializeF32Interval(serialized);
+ t.expect(
+ objectEquals(interval, deserialized),
+ `interval ${interval} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+g.test('expression_expectation').fn(t => {
+ for (const expectation of [
+ // Value
+ f32(123),
+ vec2(f32(1), f32(2)),
+ // Interval
+ toF32Interval([-0.5, 0.5]),
+ toF32Interval([kValue.f32.positive.min, kValue.f32.positive.max]),
+ // Intervals
+ [toF32Interval([-8.0, 0.5]), toF32Interval([2.0, 4.0])],
+ ]) {
+ const serialized = serializeExpectation(expectation);
+ const deserialized = deserializeExpectation(serialized);
+ t.expect(
+ objectEquals(expectation, deserialized),
+ `expectation ${expectation} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+/**
+ * Temporarily enabled building of the data cache.
+ * Required for Comparators to serialize.
+ */
+function enableBuildingDataCache(f: () => void) {
+ const wasBuildingDataCache = getIsBuildingDataCache();
+ setIsBuildingDataCache(true);
+ f();
+ setIsBuildingDataCache(wasBuildingDataCache);
+}
+
+g.test('anyOf').fn(t => {
+ enableBuildingDataCache(() => {
+ for (const c of [
+ {
+ comparator: anyOf(i32(123)),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ ]) {
+ const serialized = c.comparator as SerializedComparator;
+ const deserialized = deserializeComparator(serialized);
+ for (const val of c.testCases) {
+ const got = deserialized(val);
+ const expect = c.comparator(val);
+ t.expect(
+ got.matched === expect.matched,
+ `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}`
+ );
+ }
+ }
+ });
+});
+
+g.test('skipUndefined').fn(t => {
+ enableBuildingDataCache(() => {
+ for (const c of [
+ {
+ comparator: skipUndefined(i32(123)),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ {
+ comparator: skipUndefined(undefined),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ ]) {
+ const serialized = c.comparator as SerializedComparator;
+ const deserialized = deserializeComparator(serialized);
+ for (const val of c.testCases) {
+ const got = deserialized(val);
+ const expect = c.comparator(val);
+ t.expect(
+ got.matched === expect.matched,
+ `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}`
+ );
+ }
+ }
+ });
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts
new file mode 100644
index 0000000000..cc6dce72fb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts
@@ -0,0 +1,351 @@
+export const description = `
+Unit tests for TestGroup.
+`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { assert } from '../common/util/util.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('UnitTest_fixture').fn(async t0 => {
+ let seen = 0;
+ function count(t: Fixture): void {
+ seen++;
+ }
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('test').fn(count);
+ g.test('testp')
+ .paramsSimple([{ a: 1 }])
+ .fn(count);
+
+ await t0.run(g);
+ t0.expect(seen === 2);
+});
+
+g.test('custom_fixture').fn(async t0 => {
+ let seen = 0;
+ class Counter extends UnitTest {
+ count(): void {
+ seen++;
+ }
+ }
+
+ const g = makeTestGroupForUnitTesting(Counter);
+
+ g.test('test').fn(t => {
+ t.count();
+ });
+ g.test('testp')
+ .paramsSimple([{ a: 1 }])
+ .fn(t => {
+ t.count();
+ });
+
+ await t0.run(g);
+ t0.expect(seen === 2);
+});
+
+g.test('stack').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const doNestedThrow1 = () => {
+ throw new Error('goodbye');
+ };
+
+ const doNestedThrow2 = () => doNestedThrow1();
+
+ g.test('fail').fn(t => {
+ t.fail();
+ });
+ g.test('throw').fn(t => {
+ throw new Error('hello');
+ });
+ g.test('throw_nested').fn(t => {
+ doNestedThrow2();
+ });
+
+ const res = await t0.run(g);
+
+ const search = /unittests[/\\]test_group\.spec\.[tj]s/;
+ t0.expect(res.size > 0);
+ for (const { logs } of res.values()) {
+ assert(logs !== undefined, 'expected logs');
+ t0.expect(logs.some(l => search.test(l.toJSON())));
+ t0.expect(search.test(logs[logs.length - 1].toJSON()));
+ }
+});
+
+g.test('no_fn').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('missing');
+
+ t.shouldThrow('Error', () => {
+ g.validate();
+ });
+});
+
+g.test('duplicate_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc').fn(() => {});
+
+ t.shouldThrow('Error', () => {
+ g.test('abc').fn(() => {});
+ });
+});
+
+g.test('duplicate_test_params,none').fn(() => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([])
+ .fn(() => {});
+ g.validate();
+ }
+
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc').fn(() => {});
+ g.validate();
+ }
+
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([
+ { a: 1 }, //
+ ])
+ .fn(() => {});
+ g.validate();
+ }
+});
+
+g.test('duplicate_test_params,basic').fn(t => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('abc');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([
+ { a: 1 }, //
+ { a: 1 },
+ ]);
+ g.validate();
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .params(u =>
+ u.expandWithParams(() => [
+ { a: 1 }, //
+ { a: 1 },
+ ])
+ )
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate();
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([
+ { a: 1, b: 3 }, //
+ { b: 3, a: 1 },
+ ])
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate();
+ });
+ }
+});
+
+g.test('duplicate_test_params,with_different_private_params').fn(t => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('abc');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([
+ { a: 1, _b: 1 }, //
+ { a: 1, _b: 2 },
+ ]);
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .params(u =>
+ u.expandWithParams(() => [
+ { a: 1, _b: 1 }, //
+ { a: 1, _b: 2 },
+ ])
+ )
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate();
+ });
+ }
+});
+
+g.test('invalid_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const badChars = Array.from('"`~@#$+=\\|!^&*[]<>{}-\'. ');
+ for (const char of badChars) {
+ const name = 'a' + char + 'b';
+ t.shouldThrow(
+ 'Error',
+ () => {
+ g.test(name).fn(() => {});
+ },
+ name
+ );
+ }
+});
+
+g.test('param_value,valid').fn(() => {
+ const g = makeTestGroup(UnitTest);
+ g.test('a').paramsSimple([{ x: JSON.stringify({ a: 1, b: 2 }) }]);
+});
+
+g.test('param_value,invalid').fn(t => {
+ for (const badChar of ';=*') {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('a');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([{ badChar }]);
+ });
+ }
+});
+
+g.test('subcases').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('a')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([{ a: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.a === 1, 'a must be 1');
+ });
+
+ function* gen({ a, b }: { a?: number; b?: number }) {
+ if (b === 2) {
+ yield { ret: 2 };
+ } else if (a === 1) {
+ yield { ret: 1 };
+ } else {
+ yield { ret: -1 };
+ }
+ }
+ g.test('b')
+ .params(u =>
+ u
+ .combineWithParams([{ a: 1 }, { b: 2 }])
+ .beginSubcases()
+ .expandWithParams(gen)
+ )
+ .fn(t => {
+ const { a, b, ret } = t.params;
+ t.expect((a === 1 && ret === 1) || (b === 2 && ret === 2));
+ });
+
+ const result = await t0.run(g);
+ t0.expect(Array.from(result.values()).every(v => v.status === 'pass'));
+});
+
+g.test('exceptions')
+ .params(u =>
+ u
+ .combine('useSubcases', [false, true]) //
+ .combine('useDOMException', [false, true])
+ )
+ .fn(async t0 => {
+ const { useSubcases, useDOMException } = t0.params;
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const b1 = g.test('a');
+ let b2;
+ if (useSubcases) {
+ b2 = b1.paramsSubcasesOnly(u => u);
+ } else {
+ b2 = b1.params(u => u);
+ }
+ b2.fn(t => {
+ if (useDOMException) {
+ throw new DOMException('Message!', 'Name!');
+ } else {
+ throw new Error('Message!');
+ }
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+ });
+
+g.test('throws').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(t => {
+ throw new Error();
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
+
+g.test('shouldThrow').fn(async t0 => {
+ t0.shouldThrow('TypeError', () => {
+ throw new TypeError();
+ });
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(t => {
+ t.shouldThrow('Error', () => {
+ throw new TypeError();
+ });
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
+
+g.test('shouldReject').fn(async t0 => {
+ t0.shouldReject(
+ 'TypeError',
+ (async () => {
+ throw new TypeError();
+ })()
+ );
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(async t => {
+ t.shouldReject(
+ 'Error',
+ (async () => {
+ throw new TypeError();
+ })()
+ );
+ });
+
+ const result = await t0.run(g);
+ // Fails even though shouldReject doesn't fail until after the test function ends
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts
new file mode 100644
index 0000000000..2a939ba052
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts
@@ -0,0 +1,34 @@
+import { Logger, LogResults } from '../common/internal/logging/logger.js';
+import { TestQuerySingleCase } from '../common/internal/query/query.js';
+import { IterableTestGroup, TestCaseID } from '../common/internal/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+export class TestGroupTest extends UnitTest {
+ async run(g: IterableTestGroup): Promise<LogResults> {
+ const logger = new Logger({ overrideDebugMode: true });
+ for (const t of g.iterate()) {
+ for (const rc of t.iterate()) {
+ const query = new TestQuerySingleCase('xx', ['yy'], rc.id.test, rc.id.params);
+ const [rec] = logger.record(query.toString());
+ await rc.run(rec, query, []);
+ }
+ }
+ return logger.results;
+ }
+
+ expectCases(g: IterableTestGroup, cases: TestCaseID[]): void {
+ const gcases = [];
+ for (const t of g.iterate()) {
+ gcases.push(...Array.from(t.iterate(), c => c.id));
+ }
+ this.expect(
+ objectEquals(gcases, cases),
+ `expected
+ ${JSON.stringify(cases)}
+got
+ ${JSON.stringify(gcases)}`
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts
new file mode 100644
index 0000000000..4a744c49e9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts
@@ -0,0 +1,143 @@
+export const description = `
+Tests for TestQuery
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { parseQuery } from '../common/internal/query/parseQuery.js';
+import {
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+ TestQueryMultiCase,
+ TestQuerySingleCase,
+ TestQuery,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ expectToString(q: TestQuery, exp: string) {
+ this.expect(q.toString() === exp);
+ }
+
+ expectQueriesEqual(q1: TestQuery, q2: TestQuery) {
+ this.expect(q1.level === q2.level);
+
+ if (q1.level >= 1) {
+ this.expect(q1.isMultiFile === q2.isMultiFile);
+ this.expect(q1.suite === q2.suite);
+ this.expect(q1.filePathParts.length === q2.filePathParts.length);
+ for (let i = 0; i < q1.filePathParts.length; i++) {
+ this.expect(q1.filePathParts[i] === q2.filePathParts[i]);
+ }
+ }
+
+ if (q1.level >= 2) {
+ const p1 = q1 as TestQueryMultiTest;
+ const p2 = q2 as TestQueryMultiTest;
+
+ this.expect(p1.isMultiTest === p2.isMultiTest);
+ this.expect(p1.testPathParts.length === p2.testPathParts.length);
+ for (let i = 0; i < p1.testPathParts.length; i++) {
+ this.expect(p1.testPathParts[i] === p2.testPathParts[i]);
+ }
+ }
+
+ if (q1.level >= 3) {
+ const p1 = q1 as TestQueryMultiCase;
+ const p2 = q2 as TestQueryMultiCase;
+
+ this.expect(p1.isMultiCase === p2.isMultiCase);
+ this.expect(Object.keys(p1.params).length === Object.keys(p2.params).length);
+ for (const key of Object.keys(p1.params)) {
+ this.expect(key in p2.params);
+ const v1 = p1.params[key];
+ const v2 = p2.params[key];
+ this.expect(
+ v1 === v2 ||
+ (typeof v1 === 'number' && isNaN(v1)) === (typeof v2 === 'number' && isNaN(v2))
+ );
+ this.expect(Object.is(v1, -0) === Object.is(v2, -0));
+ }
+ }
+ }
+
+ expectQueryParse(s: string, q: TestQuery) {
+ this.expectQueriesEqual(q, parseQuery(s));
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('constructor').fn(t => {
+ t.shouldThrow('Error', () => new TestQueryMultiTest('suite', [], []));
+
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', ['a'], [], {}));
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], ['c'], {}));
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], [], {}));
+
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', ['a'], [], {}));
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], ['c'], {}));
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], [], {}));
+});
+
+g.test('toString').fn(t => {
+ t.expectToString(new TestQueryMultiFile('s', []), 's:*');
+ t.expectToString(new TestQueryMultiFile('s', ['a']), 's:a,*');
+ t.expectToString(new TestQueryMultiFile('s', ['a', 'b']), 's:a,b,*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], []), 's:a,b:*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c']), 's:a,b:c,*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd']), 's:a,b:c,d,*');
+ t.expectToString(new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:*');
+ t.expectToString(
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ 's:a,b:c,d:x=1;*'
+ );
+ t.expectToString(
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ 's:a,b:c,d:x=1;y=2;*'
+ );
+ t.expectToString(
+ new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ 's:a,b:c,d:x=1;y=2'
+ );
+ t.expectToString(new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:');
+
+ // Test handling of magic param value that convert to NaN/undefined/Infinity/etc.
+ t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN }), 's:a:b:c="_nan_"');
+ t.expectToString(
+ new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined }),
+ 's:a:b:c="_undef_"'
+ );
+ t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 }), 's:a:b:c="_negzero_"');
+});
+
+g.test('parseQuery').fn(t => {
+ t.expectQueryParse('s:*', new TestQueryMultiFile('s', []));
+ t.expectQueryParse('s:a,*', new TestQueryMultiFile('s', ['a']));
+ t.expectQueryParse('s:a,b,*', new TestQueryMultiFile('s', ['a', 'b']));
+ t.expectQueryParse('s:a,b:*', new TestQueryMultiTest('s', ['a', 'b'], []));
+ t.expectQueryParse('s:a,b:c,*', new TestQueryMultiTest('s', ['a', 'b'], ['c']));
+ t.expectQueryParse('s:a,b:c,d,*', new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd']));
+ t.expectQueryParse('s:a,b:c,d:*', new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {}));
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;*',
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 })
+ );
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;y=2;*',
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;y=2',
+ new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectQueryParse('s:a,b:c,d:', new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {}));
+
+ // Test handling of magic param value that convert to NaN/undefined/Infinity/etc.
+ t.expectQueryParse('s:a:b:c="_nan_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN }));
+ t.expectQueryParse(
+ 's:a:b:c="_undef_"',
+ new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined })
+ );
+ t.expectQueryParse('s:a:b:c="_negzero_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 }));
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts
new file mode 100644
index 0000000000..876780e151
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts
@@ -0,0 +1,3 @@
+import { Fixture } from '../common/framework/fixture.js';
+
+export class UnitTest extends Fixture {}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/README.txt
new file mode 100644
index 0000000000..c1b25dbb1c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/README.txt
@@ -0,0 +1 @@
+WebGPU conformance test suite.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/README.txt
new file mode 100644
index 0000000000..867a090a73
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/README.txt
@@ -0,0 +1 @@
+Tests for full coverage of the Javascript API surface of WebGPU.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/README.txt
new file mode 100644
index 0000000000..a6231af8b1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/README.txt
@@ -0,0 +1,2 @@
+Tests that check the result of performing valid WebGPU operations, taking advantage of
+parameterization to exercise interactions between features.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapter.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapter.spec.ts
new file mode 100644
index 0000000000..0f6ab3c31d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapter.spec.ts
@@ -0,0 +1,124 @@
+export const description = `
+Tests for GPU.requestAdapter.
+
+Test all possible options to requestAdapter.
+default, low-power, and high performance should all always return adapters.
+forceFallbackAdapter may or may not return an adapter.
+
+GPU.requestAdapter can technically return null for any reason
+but we need test functionality so the test requires an adapter except
+when forceFallbackAdapter is true.
+
+The test runs simple compute shader is run that fills a buffer with consecutive
+values and then checks the result to test the adapter for basic functionality.
+`;
+
+import { Fixture } from '../../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { getGPU } from '../../../../common/util/navigator_gpu.js';
+import { assert, objectEquals, iterRange } from '../../../../common/util/util.js';
+
+export const g = makeTestGroup(Fixture);
+
+const powerPreferenceModes: Array<GPUPowerPreference | undefined> = [
+ undefined,
+ 'low-power',
+ 'high-performance',
+];
+const forceFallbackOptions: Array<boolean | undefined> = [undefined, false, true];
+
+async function testAdapter(adapter: GPUAdapter | null) {
+ assert(adapter !== null, 'Failed to get adapter.');
+ const device = await adapter.requestDevice();
+
+ assert(device !== null, 'Failed to get device.');
+
+ const kOffset = 1230000;
+ const pipeline = device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1u) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = id.x + ${kOffset}u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const kNumElements = 64;
+ const kBufferSize = kNumElements * 4;
+ const buffer = device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const resultBuffer = device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+
+ const encoder = device.createCommandEncoder();
+
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ pass.end();
+
+ encoder.copyBufferToBuffer(buffer, 0, resultBuffer, 0, kBufferSize);
+
+ device.queue.submit([encoder.finish()]);
+
+ const expected = new Uint32Array([...iterRange(kNumElements, x => x + kOffset)]);
+
+ await resultBuffer.mapAsync(GPUMapMode.READ);
+ const actual = new Uint32Array(resultBuffer.getMappedRange());
+
+ assert(objectEquals(actual, expected), 'compute pipeline ran');
+
+ resultBuffer.destroy();
+ buffer.destroy();
+ device.destroy();
+}
+
+g.test('requestAdapter')
+ .desc(`request adapter with all possible options and check for basic functionality`)
+ .params(u =>
+ u
+ .combine('powerPreference', powerPreferenceModes)
+ .combine('forceFallbackAdapter', forceFallbackOptions)
+ )
+ .fn(async t => {
+ const { powerPreference, forceFallbackAdapter } = t.params;
+ const adapter = await getGPU().requestAdapter({
+ ...(powerPreference !== undefined && { powerPreference }),
+ ...(forceFallbackAdapter !== undefined && { forceFallbackAdapter }),
+ });
+
+ // failing to create an adapter when forceFallbackAdapter is true is ok.
+ if (forceFallbackAdapter && !adapter) {
+ t.skip('No adapter available');
+ return;
+ }
+
+ await testAdapter(adapter);
+ });
+
+g.test('requestAdapter_no_parameters')
+ .desc(`request adapter with no parameters`)
+ .fn(async () => {
+ const adapter = await getGPU().requestAdapter();
+ await testAdapter(adapter);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapterInfo.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapterInfo.spec.ts
new file mode 100644
index 0000000000..6c8c5b0dee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestAdapterInfo.spec.ts
@@ -0,0 +1,54 @@
+export const description = `
+Tests various ways of calling GPUAdapter.requestAdapterInfo.
+
+TODO:
+- Find a way to perform tests with and without user activation
+`;
+
+import { Fixture } from '../../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { getGPU } from '../../../../common/util/navigator_gpu.js';
+import { assert } from '../../../../common/util/util.js';
+
+export const g = makeTestGroup(Fixture);
+
+const normalizedIdentifierRegex = /^$|^[a-z0-9]+(-[a-z0-9]+)*$/;
+
+g.test('adapter_info')
+ .desc(
+ `
+ Test that calling requestAdapterInfo with no arguments:
+ - Returns a GPUAdapterInfo structure
+ - Every member in the structure except description is properly formatted`
+ )
+ .fn(async t => {
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ const adapterInfo = await adapter.requestAdapterInfo();
+
+ t.expect(
+ normalizedIdentifierRegex.test(adapterInfo.vendor),
+ 'adapterInfo.vendor should be a normalized identifier'
+ );
+
+ t.expect(
+ normalizedIdentifierRegex.test(adapterInfo.architecture),
+ 'adapterInfo.architecture should be a normalized identifier'
+ );
+
+ t.expect(
+ normalizedIdentifierRegex.test(adapterInfo.device),
+ 'adapterInfo.device should be a normalized identifier'
+ );
+ });
+
+g.test('adapter_info_with_hints')
+ .desc(
+ `
+ Test that calling requestAdapterInfo with hints:
+ - Rejects without user activation
+ - Succeed with user activation`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts
new file mode 100644
index 0000000000..669339439c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/adapter/requestDevice.spec.ts
@@ -0,0 +1,277 @@
+export const description = `
+Test GPUAdapter.requestDevice.
+
+Note tests explicitly destroy created devices so that tests don't have to wait for GC to clean up
+potentially limited native resources.
+`;
+
+import { Fixture } from '../../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { getGPU } from '../../../../common/util/navigator_gpu.js';
+import { assert, raceWithRejectOnTimeout } from '../../../../common/util/util.js';
+import { kFeatureNames, kLimitInfo, kLimits } from '../../../capability_info.js';
+import { clamp, isPowerOfTwo } from '../../../util/math.js';
+
+export const g = makeTestGroup(Fixture);
+
+g.test('default')
+ .desc(
+ `
+ Test requesting the device with a variation of default paramters.
+ - No features listed in default device
+ - Default limits`
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('args', [
+ [],
+ [undefined],
+ [{}],
+ [{ requiredFeatures: [], requiredLimits: {} }],
+ ] as const)
+ )
+ .fn(async t => {
+ const { args } = t.params;
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+ const device = await adapter.requestDevice(...args);
+ assert(device !== null);
+
+ // Default device should have no features.
+ t.expect(device.features.size === 0, 'Default device should not have any features');
+ // All limits should be defaults.
+ for (const limit of kLimits) {
+ t.expect(
+ device.limits[limit] === kLimitInfo[limit].default,
+ `Expected ${limit} == default: ${device.limits[limit]} != ${kLimitInfo[limit].default}`
+ );
+ }
+
+ device.destroy();
+ });
+
+g.test('invalid')
+ .desc(
+ `
+ Test that requesting device on an invalid adapter resolves with lost device.
+ - Induce invalid adapter via a device lost from a device.destroy()`
+ )
+ .fn(async t => {
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ {
+ // Request a device and destroy it immediately afterwards.
+ const device = await adapter.requestDevice();
+ assert(device !== null);
+ device.destroy();
+ const lostInfo = await device.lost;
+ t.expect(lostInfo.reason === 'destroyed');
+ }
+
+ // The adapter should now be invalid since a device was lost. Requesting another device should
+ // return an already lost device.
+ const kTimeoutMS = 1000;
+ const device = await adapter.requestDevice();
+ const lost = await raceWithRejectOnTimeout(device.lost, kTimeoutMS, 'device was not lost');
+ t.expect(lost.reason === undefined);
+ });
+
+g.test('features,unknown')
+ .desc(
+ `
+ Test requesting device with an unknown feature.`
+ )
+ .fn(async t => {
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ t.shouldReject(
+ 'TypeError',
+ adapter.requestDevice({ requiredFeatures: ['unknown-feature' as GPUFeatureName] })
+ );
+ });
+
+g.test('features,known')
+ .desc(
+ `
+ Test requesting device with all features.
+ - Succeeds with device supporting feature if adapter supports the feature.
+ - Rejects if the adapter does not support the feature.`
+ )
+ .params(u => u.combine('feature', kFeatureNames))
+ .fn(async t => {
+ const { feature } = t.params;
+
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ const promise = adapter.requestDevice({ requiredFeatures: [feature] });
+ if (adapter.features.has(feature)) {
+ const device = await promise;
+ t.expect(device.features.has(feature), 'Device should include the required feature');
+ } else {
+ t.shouldReject('TypeError', promise);
+ }
+ });
+
+g.test('limits,unknown')
+ .desc(
+ `
+ Test that specifying limits that aren't part of the supported limit set causes
+ requestDevice to reject.`
+ )
+ .fn(async t => {
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ const requiredLimits: Record<string, number> = { unknownLimitName: 9000 };
+
+ t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
+ });
+
+g.test('limits,supported')
+ .desc(
+ `
+ Test that each supported limit can be specified with valid values.
+ - Tests each limit with the default values given by the spec
+ - Tests each limit with the supported values given by the adapter`
+ )
+ .params(u =>
+ u.combine('limit', kLimits).beginSubcases().combine('limitValue', ['default', 'adapter'])
+ )
+ .fn(async t => {
+ const { limit, limitValue } = t.params;
+
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ let value: number = -1;
+ switch (limitValue) {
+ case 'default':
+ value = kLimitInfo[limit].default;
+ break;
+ case 'adapter':
+ value = adapter.limits[limit];
+ break;
+ }
+
+ const device = await adapter.requestDevice({ requiredLimits: { [limit]: value } });
+ assert(device !== null);
+ t.expect(
+ device.limits[limit] === value,
+ 'Devices reported limit should match the required limit'
+ );
+ device.destroy();
+ });
+
+g.test('limit,better_than_supported')
+ .desc(
+ `
+ Test that specifying a better limit than what the adapter supports causes requestDevice to
+ reject.
+ - Tests each limit
+ - Tests requesting better limits by various amounts`
+ )
+ .params(u =>
+ u
+ .combine('limit', kLimits)
+ .beginSubcases()
+ .expandWithParams(p => {
+ switch (kLimitInfo[p.limit].class) {
+ case 'maximum':
+ return [
+ { mul: 1, add: 1 },
+ { mul: 1, add: 100 },
+ ];
+ case 'alignment':
+ return [
+ { mul: 1, add: -1 },
+ { mul: 1 / 2, add: 0 },
+ { mul: 1 / 1024, add: 0 },
+ ];
+ }
+ })
+ )
+ .fn(async t => {
+ const { limit, mul, add } = t.params;
+
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ const value = adapter.limits[limit] * mul + add;
+ const requiredLimits = {
+ [limit]: clamp(value, { min: 0, max: kLimitInfo[limit].maximumValue }),
+ };
+
+ t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
+ });
+
+g.test('limit,worse_than_default')
+ .desc(
+ `
+ Test that specifying a worse limit than the default values required by the spec cause the value
+ to clamp.
+ - Tests each limit
+ - Tests requesting worse limits by various amounts`
+ )
+ .params(u =>
+ u
+ .combine('limit', kLimits)
+ .beginSubcases()
+ .expandWithParams(p => {
+ switch (kLimitInfo[p.limit].class) {
+ case 'maximum':
+ return [
+ { mul: 1, add: -1 },
+ { mul: 1, add: -100 },
+ ];
+ case 'alignment':
+ return [
+ { mul: 1, add: 1 },
+ { mul: 2, add: 0 },
+ { mul: 1024, add: 0 },
+ ];
+ }
+ })
+ )
+ .fn(async t => {
+ const { limit, mul, add } = t.params;
+
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+
+ const value = kLimitInfo[limit].default * mul + add;
+ const requiredLimits = {
+ [limit]: clamp(value, { min: 0, max: kLimitInfo[limit].maximumValue }),
+ };
+
+ let success;
+ switch (kLimitInfo[limit].class) {
+ case 'alignment':
+ success = isPowerOfTwo(value);
+ break;
+ case 'maximum':
+ success = true;
+ break;
+ }
+
+ if (success) {
+ const device = await adapter.requestDevice({ requiredLimits });
+ assert(device !== null);
+ t.expect(
+ device.limits[limit] === kLimitInfo[limit].default,
+ 'Devices reported limit should match the default limit'
+ );
+ device.destroy();
+ } else {
+ t.shouldReject('OperationError', adapter.requestDevice({ requiredLimits }));
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/async_ordering/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/async_ordering/README.txt
new file mode 100644
index 0000000000..caaf5ba5ff
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/async_ordering/README.txt
@@ -0,0 +1,12 @@
+Test ordering of async resolutions between promises returned by the following calls (and possibly
+between multiple of the same call), where there are constraints on the ordering.
+Spec issue: https://github.com/gpuweb/gpuweb/issues/962
+
+TODO: plan and implement
+- createReadyPipeline() (not sure if this actually has any ordering constraints)
+- cmdbuf.executionTime
+- device.popErrorScope()
+- device.lost
+- queue.onSubmittedWorkDone()
+- buffer.mapAsync()
+- shadermodule.compilationInfo()
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/README.txt
new file mode 100644
index 0000000000..b5d99e646b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/README.txt
@@ -0,0 +1 @@
+GPUBuffer tests.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts
new file mode 100644
index 0000000000..c0cdb3acfa
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts
@@ -0,0 +1,499 @@
+export const description = `
+Test the operation of buffer mapping, specifically the data contents written via
+map-write/mappedAtCreation, and the contents of buffers returned by getMappedRange on
+buffers which are mapped-read/mapped-write/mappedAtCreation.
+
+range: used for getMappedRange
+mapRegion: used for mapAsync
+
+mapRegionBoundModes is used to get mapRegion from range:
+ - default-expand: expand mapRegion to buffer bound by setting offset/size to undefined
+ - explicit-expand: expand mapRegion to buffer bound by explicitly calculating offset/size
+ - minimal: make mapRegion to be the same as range which is the minimal range to make getMappedRange input valid
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, memcpy } from '../../../../common/util/util.js';
+import { checkElementsEqual } from '../../../util/check_contents.js';
+
+import { MappingTest } from './mapping_test.js';
+
+export const g = makeTestGroup(MappingTest);
+
+const kSubcases = [
+ { size: 0, range: [] },
+ { size: 0, range: [undefined] },
+ { size: 0, range: [undefined, undefined] },
+ { size: 0, range: [0] },
+ { size: 0, range: [0, undefined] },
+ { size: 0, range: [0, 0] },
+ { size: 12, range: [] },
+ { size: 12, range: [undefined] },
+ { size: 12, range: [undefined, undefined] },
+ { size: 12, range: [0] },
+ { size: 12, range: [0, undefined] },
+ { size: 12, range: [0, 12] },
+ { size: 12, range: [0, 0] },
+ { size: 12, range: [8] },
+ { size: 12, range: [8, undefined] },
+ { size: 12, range: [8, 4] },
+ { size: 28, range: [8, 8] },
+ { size: 28, range: [8, 12] },
+ { size: 512 * 1024, range: [] },
+] as const;
+
+function reifyMapRange(bufferSize: number, range: readonly [number?, number?]): [number, number] {
+ const offset = range[0] ?? 0;
+ return [offset, range[1] ?? bufferSize - offset];
+}
+
+const mapRegionBoundModes = ['default-expand', 'explicit-expand', 'minimal'] as const;
+type MapRegionBoundMode = typeof mapRegionBoundModes[number];
+
+function getRegionForMap(
+ bufferSize: number,
+ range: [number, number],
+ {
+ mapAsyncRegionLeft,
+ mapAsyncRegionRight,
+ }: {
+ mapAsyncRegionLeft: MapRegionBoundMode;
+ mapAsyncRegionRight: MapRegionBoundMode;
+ }
+) {
+ const regionLeft = mapAsyncRegionLeft === 'minimal' ? range[0] : 0;
+ const regionRight = mapAsyncRegionRight === 'minimal' ? range[0] + range[1] : bufferSize;
+ return [
+ mapAsyncRegionLeft === 'default-expand' ? undefined : regionLeft,
+ mapAsyncRegionRight === 'default-expand' ? undefined : regionRight - regionLeft,
+ ] as const;
+}
+
+g.test('mapAsync,write')
+ .desc(
+ `Use map-write to write to various ranges of variously-sized buffers, then expectContents
+(which does copyBufferToBuffer + map-read) to ensure the contents were written.`
+ )
+ .params(u =>
+ u
+ .combine('mapAsyncRegionLeft', mapRegionBoundModes)
+ .combine('mapAsyncRegionRight', mapRegionBoundModes)
+ .beginSubcases()
+ .combineWithParams(kSubcases)
+ )
+ .fn(async t => {
+ const { size, range } = t.params;
+ const [rangeOffset, rangeSize] = reifyMapRange(size, range);
+
+ const buffer = t.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ });
+
+ const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params);
+ await buffer.mapAsync(GPUMapMode.WRITE, ...mapRegion);
+ const arrayBuffer = buffer.getMappedRange(...range);
+ t.checkMapWrite(buffer, rangeOffset, arrayBuffer, rangeSize);
+ });
+
+g.test('mapAsync,write,unchanged_ranges_preserved')
+ .desc(
+ `Use mappedAtCreation or mapAsync to write to various ranges of variously-sized buffers, then
+use mapAsync to map a different range and zero it out. Finally use expectGPUBufferValuesEqual
+(which does copyBufferToBuffer + map-read) to verify that contents originally written outside the
+second mapped range were not altered.`
+ )
+ .params(u =>
+ u
+ .beginSubcases()
+ .combine('mappedAtCreation', [false, true])
+ .combineWithParams([
+ { size: 12, range1: [], range2: [8] },
+ { size: 12, range1: [], range2: [0, 8] },
+ { size: 12, range1: [0, 8], range2: [8] },
+ { size: 12, range1: [8], range2: [0, 8] },
+ { size: 28, range1: [], range2: [8, 8] },
+ { size: 28, range1: [8, 16], range2: [16, 8] },
+ { size: 32, range1: [16, 12], range2: [8, 16] },
+ { size: 32, range1: [8, 8], range2: [24, 4] },
+ ] as const)
+ )
+ .fn(async t => {
+ const { size, range1, range2, mappedAtCreation } = t.params;
+ const [rangeOffset1, rangeSize1] = reifyMapRange(size, range1);
+ const [rangeOffset2, rangeSize2] = reifyMapRange(size, range2);
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation,
+ size,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ });
+
+ // If the buffer is not mappedAtCreation map it now.
+ if (!mappedAtCreation) {
+ await buffer.mapAsync(GPUMapMode.WRITE);
+ }
+
+ // Set the initial contents of the buffer.
+ const init = buffer.getMappedRange(...range1);
+
+ assert(init.byteLength === rangeSize1);
+ const expectedBuffer = new ArrayBuffer(size);
+ const expected = new Uint32Array(
+ expectedBuffer,
+ rangeOffset1,
+ rangeSize1 / Uint32Array.BYTES_PER_ELEMENT
+ );
+ const data = new Uint32Array(init);
+ for (let i = 0; i < data.length; ++i) {
+ data[i] = expected[i] = i + 1;
+ }
+ buffer.unmap();
+
+ // Write to a second range of the buffer
+ await buffer.mapAsync(GPUMapMode.WRITE, ...range2);
+ const init2 = buffer.getMappedRange(...range2);
+
+ assert(init2.byteLength === rangeSize2);
+ const expected2 = new Uint32Array(
+ expectedBuffer,
+ rangeOffset2,
+ rangeSize2 / Uint32Array.BYTES_PER_ELEMENT
+ );
+ const data2 = new Uint32Array(init2);
+ for (let i = 0; i < data2.length; ++i) {
+ data2[i] = expected2[i] = 0;
+ }
+ buffer.unmap();
+
+ // Verify that the range of the buffer which was not overwritten was preserved.
+ t.expectGPUBufferValuesEqual(buffer, expected, rangeOffset1);
+ });
+
+g.test('mapAsync,read')
+ .desc(
+ `Use mappedAtCreation to initialize various ranges of variously-sized buffers, then
+map-read and check the read-back result.`
+ )
+ .params(u =>
+ u
+ .combine('mapAsyncRegionLeft', mapRegionBoundModes)
+ .combine('mapAsyncRegionRight', mapRegionBoundModes)
+ .beginSubcases()
+ .combineWithParams(kSubcases)
+ )
+ .fn(async t => {
+ const { size, range } = t.params;
+ const [rangeOffset, rangeSize] = reifyMapRange(size, range);
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ });
+ const init = buffer.getMappedRange(...range);
+
+ assert(init.byteLength === rangeSize);
+ const expected = new Uint32Array(new ArrayBuffer(rangeSize));
+ const data = new Uint32Array(init);
+ for (let i = 0; i < data.length; ++i) {
+ data[i] = expected[i] = i + 1;
+ }
+ buffer.unmap();
+
+ const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params);
+ await buffer.mapAsync(GPUMapMode.READ, ...mapRegion);
+ const actual = new Uint8Array(buffer.getMappedRange(...range));
+ t.expectOK(checkElementsEqual(actual, new Uint8Array(expected.buffer)));
+ });
+
+g.test('mapAsync,read,typedArrayAccess')
+ .desc(`Use various TypedArray types to read back from a mapped buffer`)
+ .params(u =>
+ u
+ .combine('mapAsyncRegionLeft', mapRegionBoundModes)
+ .combine('mapAsyncRegionRight', mapRegionBoundModes)
+ .beginSubcases()
+ .combineWithParams([
+ { size: 80, range: [] },
+ { size: 160, range: [] },
+ { size: 160, range: [0, 80] },
+ { size: 160, range: [80] },
+ { size: 160, range: [40, 120] },
+ { size: 160, range: [40] },
+ ] as const)
+ )
+ .fn(async t => {
+ const { size, range } = t.params;
+ const [rangeOffset, rangeSize] = reifyMapRange(size, range);
+
+ // Fill an array buffer with a variety of values of different types.
+ const expectedArrayBuffer = new ArrayBuffer(80);
+ const uint8Expected = new Uint8Array(expectedArrayBuffer, 0, 2);
+ uint8Expected[0] = 1;
+ uint8Expected[1] = 255;
+
+ const int8Expected = new Int8Array(expectedArrayBuffer, 2, 2);
+ int8Expected[0] = -1;
+ int8Expected[1] = 127;
+
+ const uint16Expected = new Uint16Array(expectedArrayBuffer, 4, 2);
+ uint16Expected[0] = 1;
+ uint16Expected[1] = 65535;
+
+ const int16Expected = new Int16Array(expectedArrayBuffer, 8, 2);
+ int16Expected[0] = -1;
+ int16Expected[1] = 32767;
+
+ const uint32Expected = new Uint32Array(expectedArrayBuffer, 12, 2);
+ uint32Expected[0] = 1;
+ uint32Expected[1] = 4294967295;
+
+ const int32Expected = new Int32Array(expectedArrayBuffer, 20, 2);
+ int32Expected[2] = -1;
+ int32Expected[3] = 2147483647;
+
+ const float32Expected = new Float32Array(expectedArrayBuffer, 28, 3);
+ float32Expected[0] = 1;
+ float32Expected[1] = -1;
+ float32Expected[2] = 12345.6789;
+
+ const float64Expected = new Float64Array(expectedArrayBuffer, 40, 5);
+ float64Expected[0] = 1;
+ float64Expected[1] = -1;
+ float64Expected[2] = 12345.6789;
+ float64Expected[3] = Number.MAX_VALUE;
+ float64Expected[4] = Number.MIN_VALUE;
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ });
+ const init = buffer.getMappedRange(...range);
+
+ // Copy the expected values into the mapped range.
+ assert(init.byteLength === rangeSize);
+ memcpy({ src: expectedArrayBuffer }, { dst: init });
+ buffer.unmap();
+
+ const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params);
+ await buffer.mapAsync(GPUMapMode.READ, ...mapRegion);
+ const mappedArrayBuffer = buffer.getMappedRange(...range);
+ t.expectOK(checkElementsEqual(new Uint8Array(mappedArrayBuffer, 0, 2), uint8Expected));
+ t.expectOK(checkElementsEqual(new Int8Array(mappedArrayBuffer, 2, 2), int8Expected));
+ t.expectOK(checkElementsEqual(new Uint16Array(mappedArrayBuffer, 4, 2), uint16Expected));
+ t.expectOK(checkElementsEqual(new Int16Array(mappedArrayBuffer, 8, 2), int16Expected));
+ t.expectOK(checkElementsEqual(new Uint32Array(mappedArrayBuffer, 12, 2), uint32Expected));
+ t.expectOK(checkElementsEqual(new Int32Array(mappedArrayBuffer, 20, 2), int32Expected));
+ t.expectOK(checkElementsEqual(new Float32Array(mappedArrayBuffer, 28, 3), float32Expected));
+ t.expectOK(checkElementsEqual(new Float64Array(mappedArrayBuffer, 40, 5), float64Expected));
+ });
+
+g.test('mappedAtCreation')
+ .desc(
+ `Use mappedAtCreation to write to various ranges of variously-sized buffers created either
+with or without the MAP_WRITE usage (since this could affect the mappedAtCreation upload path),
+then expectContents (which does copyBufferToBuffer + map-read) to ensure the contents were written.`
+ )
+ .params(u =>
+ u //
+ .combine('mappable', [false, true])
+ .beginSubcases()
+ .combineWithParams(kSubcases)
+ )
+ .fn(async t => {
+ const { size, range, mappable } = t.params;
+ const [, rangeSize] = reifyMapRange(size, range);
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size,
+ usage: GPUBufferUsage.COPY_SRC | (mappable ? GPUBufferUsage.MAP_WRITE : 0),
+ });
+ const arrayBuffer = buffer.getMappedRange(...range);
+ t.checkMapWrite(buffer, range[0] ?? 0, arrayBuffer, rangeSize);
+ });
+
+g.test('remapped_for_write')
+ .desc(
+ `Use mappedAtCreation or mapAsync to write to various ranges of variously-sized buffers created
+with the MAP_WRITE usage, then mapAsync again and ensure that the previously written values are
+still present in the mapped buffer.`
+ )
+ .params(u =>
+ u //
+ .combine('mapAsyncRegionLeft', mapRegionBoundModes)
+ .combine('mapAsyncRegionRight', mapRegionBoundModes)
+ .beginSubcases()
+ .combine('mappedAtCreation', [false, true])
+ .combineWithParams(kSubcases)
+ )
+ .fn(async t => {
+ const { size, range, mappedAtCreation } = t.params;
+ const [rangeOffset, rangeSize] = reifyMapRange(size, range);
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation,
+ size,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ });
+
+ // If the buffer is not mappedAtCreation map it now.
+ if (!mappedAtCreation) {
+ await buffer.mapAsync(GPUMapMode.WRITE);
+ }
+
+ // Set the initial contents of the buffer.
+ const init = buffer.getMappedRange(...range);
+
+ assert(init.byteLength === rangeSize);
+ const expected = new Uint32Array(new ArrayBuffer(rangeSize));
+ const data = new Uint32Array(init);
+ for (let i = 0; i < data.length; ++i) {
+ data[i] = expected[i] = i + 1;
+ }
+ buffer.unmap();
+
+ // Check that upon remapping the for WRITE the values in the buffer are
+ // still the same.
+ const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params);
+ await buffer.mapAsync(GPUMapMode.WRITE, ...mapRegion);
+ const actual = new Uint8Array(buffer.getMappedRange(...range));
+ t.expectOK(checkElementsEqual(actual, new Uint8Array(expected.buffer)));
+ });
+
+g.test('mappedAtCreation,mapState')
+ .desc('Test that exposed map state of buffer created with mappedAtCreation has expected values.')
+ .params(u =>
+ u
+ .combine('validationError', [false, true])
+ .combine('afterUnmap', [false, true])
+ .combine('afterDestroy', [false, true])
+ )
+ .fn(async t => {
+ const { validationError, afterUnmap, afterDestroy } = t.params;
+ const size = 8;
+ const range = [0, 8];
+
+ let buffer: GPUBuffer;
+ t.expectValidationError(() => {
+ buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size,
+ usage: validationError ? 0 : GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ });
+ }, validationError);
+
+ // mapState must be "mapped" regardless of validation error
+ t.expect(buffer!.mapState === 'mapped');
+
+ // getMappedRange must not change the map state
+ buffer!.getMappedRange(...range);
+ t.expect(buffer!.mapState === 'mapped');
+
+ if (afterUnmap) {
+ buffer!.unmap();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+
+ if (afterDestroy) {
+ buffer!.destroy();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+ });
+
+g.test('mapAsync,mapState')
+ .desc('Test that exposed map state of buffer mapped with mapAsync has expected values.')
+ .params(u =>
+ u
+ .combine('bufferCreationValidationError', [false, true])
+ .combine('mapAsyncValidationError', [false, true])
+ .combine('beforeUnmap', [false, true])
+ .combine('beforeDestroy', [false, true])
+ .combine('afterUnmap', [false, true])
+ .combine('afterDestroy', [false, true])
+ )
+ .fn(async t => {
+ const {
+ bufferCreationValidationError,
+ mapAsyncValidationError,
+ beforeUnmap,
+ beforeDestroy,
+ afterUnmap,
+ afterDestroy,
+ } = t.params;
+ const size = 8;
+ const range = [0, 8];
+
+ let buffer: GPUBuffer;
+ t.expectValidationError(() => {
+ buffer = t.device.createBuffer({
+ mappedAtCreation: false,
+ size,
+ usage: bufferCreationValidationError
+ ? 0
+ : GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ });
+ }, bufferCreationValidationError);
+
+ t.expect(buffer!.mapState === 'unmapped');
+
+ {
+ let promise: Promise<void>;
+ t.expectValidationError(() => {
+ promise = buffer!.mapAsync(mapAsyncValidationError ? 0 : GPUMapMode.WRITE);
+ }, bufferCreationValidationError || mapAsyncValidationError);
+ t.expect(buffer!.mapState === 'pending');
+
+ try {
+ if (beforeUnmap) {
+ buffer!.unmap();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+ if (beforeDestroy) {
+ buffer!.destroy();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+
+ await promise!;
+ t.expect(buffer!.mapState === 'mapped');
+
+ // getMappedRange must not change the map state
+ buffer!.getMappedRange(...range);
+ t.expect(buffer!.mapState === 'mapped');
+ } catch {
+ // unmapped before resolve, destroyed before resolve, or mapAsync validation error
+ // will end up with rejection and 'unmapped'
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+ }
+
+ // If buffer is already mapped test mapAsync on already mapped buffer
+ if (buffer!.mapState === 'mapped') {
+ // mapAsync on already mapped buffer must be rejected with a validation error
+ // and the map state must keep 'mapped'
+ let promise: Promise<void>;
+ t.expectValidationError(() => {
+ promise = buffer!.mapAsync(GPUMapMode.WRITE);
+ }, true);
+ t.expect(buffer!.mapState === 'mapped');
+
+ try {
+ await promise!;
+ t.fail('mapAsync on already mapped buffer must not succeed.');
+ } catch {
+ t.expect(buffer!.mapState === 'mapped');
+ }
+ }
+
+ if (afterUnmap) {
+ buffer!.unmap();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+
+ if (afterDestroy) {
+ buffer!.destroy();
+ t.expect(buffer!.mapState === 'unmapped');
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_ArrayBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_ArrayBuffer.spec.ts
new file mode 100644
index 0000000000..fc0bfac39d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_ArrayBuffer.spec.ts
@@ -0,0 +1,89 @@
+export const description = `
+Tests for the behavior of ArrayBuffers returned by getMappedRange.
+
+TODO: Add tests that transfer to another thread instead of just using MessageChannel.
+TODO: Add tests for any other Web APIs that can detach ArrayBuffers.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { timeout } from '../../../../common/util/timeout.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { checkElementsEqual } from '../../../util/check_contents.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('postMessage')
+ .desc(
+ `Using postMessage to send a getMappedRange-returned ArrayBuffer throws a TypeError
+ if it was included in the transfer list. Otherwise, it makes a copy.
+ Test combinations of transfer={false, true}, mapMode={read,write}.`
+ )
+ .params(u =>
+ u //
+ .combine('transfer', [false, true])
+ .combine('mapMode', ['READ', 'WRITE'] as const)
+ )
+ .fn(async t => {
+ const { transfer, mapMode } = t.params;
+ const kSize = 1024;
+
+ // Populate initial data.
+ const initialData = new Uint32Array(new ArrayBuffer(kSize));
+ for (let i = 0; i < initialData.length; ++i) {
+ initialData[i] = i;
+ }
+
+ const buf = t.makeBufferWithContents(
+ initialData,
+ mapMode === 'WRITE' ? GPUBufferUsage.MAP_WRITE : GPUBufferUsage.MAP_READ
+ );
+
+ await buf.mapAsync(GPUMapMode[mapMode]);
+ const ab1 = buf.getMappedRange();
+ t.expect(ab1.byteLength === kSize, 'ab1 should have the size of the buffer');
+
+ const mc = new MessageChannel();
+ const ab2Promise = new Promise<ArrayBuffer>(resolve => {
+ mc.port2.onmessage = ev => {
+ if (transfer) {
+ t.fail(
+ `postMessage with ab1 in transfer list should not be received. Unexpected message: ${ev.data}`
+ );
+ } else {
+ resolve(ev.data);
+ }
+ };
+ });
+
+ if (transfer) {
+ t.shouldThrow('TypeError', () => mc.port1.postMessage(ab1, [ab1]));
+ // Wait to make sure the postMessage isn't received.
+ await new Promise(resolve => timeout(resolve, 100));
+ } else {
+ mc.port1.postMessage(ab1);
+ }
+ t.expect(ab1.byteLength === kSize, 'after postMessage, ab1 should not be detached');
+
+ if (!transfer) {
+ const ab2 = await ab2Promise;
+ t.expect(ab2.byteLength === kSize, 'ab2 should be the same size');
+ const ab2Data = new Uint32Array(ab2, 0, initialData.length);
+ // ab2 should have the same initial contents.
+ t.expectOK(checkElementsEqual(ab2Data, initialData));
+
+ // Mutations to ab2 should not be visible in ab1.
+ const ab1Data = new Uint32Array(ab1, 0, initialData.length);
+ const abs2NewData = initialData.slice().reverse();
+ for (let i = 0; i < ab2Data.length; ++i) {
+ ab2Data[i] = abs2NewData[i];
+ }
+ t.expectOK(checkElementsEqual(ab1Data, initialData));
+ t.expectOK(checkElementsEqual(ab2Data, abs2NewData));
+ }
+
+ buf.unmap();
+ t.expect(ab1.byteLength === 0, 'after unmap, ab1 should be detached');
+
+ // Transferring an already-detached ArrayBuffer is a DataCloneError.
+ t.shouldThrow('DataCloneError', () => mc.port1.postMessage(ab1, [ab1]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_detach.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_detach.spec.ts
new file mode 100644
index 0000000000..52479f89f6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_detach.spec.ts
@@ -0,0 +1,79 @@
+export const description = `
+ Tests that TypedArrays created when mapping a GPUBuffer are detached when the
+ buffer is unmapped or destroyed.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { getGPU } from '../../../../common/util/navigator_gpu.js';
+import { assert } from '../../../../common/util/util.js';
+import { GPUConst } from '../../../constants.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('while_mapped')
+ .desc(
+ `
+ Test that a mapped buffers are able to properly detach.
+ - Tests {mappable, unmappable mapAtCreation, mappable mapAtCreation}
+ - Tests while {mapped, mapped at creation, mapped at creation then unmapped and mapped again}
+ - When {unmap, destroy, unmap && destroy, device.destroy} is called`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('mappedAtCreation', [false, true])
+ .combineWithParams([
+ { usage: GPUConst.BufferUsage.COPY_SRC },
+ { usage: GPUConst.BufferUsage.MAP_WRITE | GPUConst.BufferUsage.COPY_SRC },
+ { usage: GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ },
+ {
+ usage: GPUConst.BufferUsage.MAP_WRITE | GPUConst.BufferUsage.COPY_SRC,
+ mapMode: GPUConst.MapMode.WRITE,
+ },
+ {
+ usage: GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ,
+ mapMode: GPUConst.MapMode.READ,
+ },
+ ])
+ .combineWithParams([
+ { unmap: true, destroy: false },
+ { unmap: false, destroy: true },
+ { unmap: true, destroy: true },
+ { unmap: false, destroy: false, deviceDestroy: true },
+ ])
+ .unless(p => p.mappedAtCreation === false && p.mapMode === undefined)
+ )
+ .fn(async t => {
+ const { usage, mapMode, mappedAtCreation, unmap, destroy, deviceDestroy } = t.params;
+
+ let device: GPUDevice = t.device;
+ if (deviceDestroy) {
+ const adapter = await getGPU().requestAdapter();
+ assert(adapter !== null);
+ device = await adapter.requestDevice();
+ }
+ const buffer = device.createBuffer({
+ size: 4,
+ usage,
+ mappedAtCreation,
+ });
+
+ if (mapMode !== undefined) {
+ if (mappedAtCreation) {
+ buffer.unmap();
+ }
+ await buffer.mapAsync(mapMode);
+ }
+
+ const arrayBuffer = buffer.getMappedRange();
+ const view = new Uint8Array(arrayBuffer);
+ t.expect(arrayBuffer.byteLength === 4);
+ t.expect(view.length === 4);
+
+ if (unmap) buffer.unmap();
+ if (destroy) buffer.destroy();
+ if (deviceDestroy) device.destroy();
+
+ t.expect(arrayBuffer.byteLength === 0, 'ArrayBuffer should be detached');
+ t.expect(view.byteLength === 0, 'ArrayBufferView should be detached');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_oom.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_oom.spec.ts
new file mode 100644
index 0000000000..ba4ddfb5c3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map_oom.spec.ts
@@ -0,0 +1,120 @@
+export const description =
+ 'Test out-of-memory conditions creating large mappable/mappedAtCreation buffers.';
+
+import { kUnitCaseParamsBuilder } from '../../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kBufferUsages } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { kMaxSafeMultipleOf8 } from '../../../util/math.js';
+
+const oomAndSizeParams = kUnitCaseParamsBuilder
+ .combine('oom', [false, true])
+ .expand('size', ({ oom }) => {
+ return oom
+ ? [
+ kMaxSafeMultipleOf8,
+ 0x20_0000_0000, // 128 GB
+ ]
+ : [16];
+ });
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('mapAsync')
+ .desc(
+ `Test creating a large mappable buffer should produce an out-of-memory error if allocation fails.
+ - The resulting buffer is an error buffer, so mapAsync rejects and produces a validation error.
+ - Calling getMappedRange should throw an OperationError because the buffer is not in the mapped state.
+ - unmap() doesn't throw an error even if mapping failed, and otherwise should detach the ArrayBuffer.
+`
+ )
+ .params(
+ oomAndSizeParams //
+ .beginSubcases()
+ .combine('write', [false, true])
+ .combine('unmapBeforeResolve', [false, true])
+ )
+ .fn(async t => {
+ const { oom, write, size, unmapBeforeResolve } = t.params;
+
+ const buffer = t.expectGPUError(
+ 'out-of-memory',
+ () =>
+ t.device.createBuffer({
+ size,
+ usage: write ? GPUBufferUsage.MAP_WRITE : GPUBufferUsage.MAP_READ,
+ }),
+ oom
+ );
+
+ let promise: Promise<void>;
+ // Should be a validation error since the buffer is invalid.
+ // Unmap abort error shouldn't cause a validation error.
+ t.expectValidationError(() => {
+ promise = buffer.mapAsync(write ? GPUMapMode.WRITE : GPUMapMode.READ);
+ }, oom);
+
+ if (oom) {
+ if (unmapBeforeResolve) {
+ // Should reject with abort error because buffer will be unmapped
+ // before validation check finishes.
+ t.shouldReject('AbortError', promise!);
+ } else {
+ // Should also reject in addition to the validation error.
+ t.shouldReject('OperationError', promise!);
+
+ // Wait for validation error before unmap to ensure validation check
+ // ends before unmap.
+ try {
+ await promise!;
+ throw new Error('The promise should be rejected.');
+ } catch {
+ // Should cause an exception because the promise should be rejected.
+ }
+ }
+
+ // Should throw an OperationError because the buffer is not mapped.
+ // Note: not a RangeError because the state of the buffer is checked first.
+ t.shouldThrow('OperationError', () => {
+ buffer.getMappedRange();
+ });
+
+ // Should't be a validation error even if the buffer failed to be mapped.
+ buffer.unmap();
+ } else {
+ await promise!;
+ const arraybuffer = buffer.getMappedRange();
+ t.expect(arraybuffer.byteLength === size);
+ buffer.unmap();
+ t.expect(arraybuffer.byteLength === 0, 'Mapping should be detached');
+ }
+ });
+
+g.test('mappedAtCreation')
+ .desc(
+ `Test creating a very large buffer mappedAtCreation buffer should throw a RangeError only
+ because such a large allocation cannot be created when we initialize an active buffer mapping.
+`
+ )
+ .params(
+ oomAndSizeParams //
+ .beginSubcases()
+ .combine('usage', kBufferUsages)
+ )
+ .fn(async t => {
+ const { oom, usage, size } = t.params;
+
+ const f = () => t.device.createBuffer({ mappedAtCreation: true, size, usage });
+
+ if (oom) {
+ // getMappedRange is normally valid on OOM buffers, but this one fails because the
+ // (default) range is too large to create the returned ArrayBuffer.
+ t.shouldThrow('RangeError', f);
+ } else {
+ const buffer = f();
+ const mapping = buffer.getMappedRange();
+ t.expect(mapping.byteLength === size, 'Mapping should be successful');
+ buffer.unmap();
+ t.expect(mapping.byteLength === 0, 'Mapping should be detached');
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/mapping_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/mapping_test.ts
new file mode 100644
index 0000000000..733e2dcb69
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/mapping_test.ts
@@ -0,0 +1,39 @@
+import { assert } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export class MappingTest extends GPUTest {
+ checkMapWrite(
+ buffer: GPUBuffer,
+ offset: number,
+ mappedContents: ArrayBuffer,
+ size: number
+ ): void {
+ this.checkMapWriteZeroed(mappedContents, size);
+
+ const mappedView = new Uint32Array(mappedContents);
+ const expected = new Uint32Array(new ArrayBuffer(size));
+ assert(mappedView.byteLength === size);
+ for (let i = 0; i < mappedView.length; ++i) {
+ mappedView[i] = expected[i] = i + 1;
+ }
+ buffer.unmap();
+
+ this.expectGPUBufferValuesEqual(buffer, expected, offset);
+ }
+
+ checkMapWriteZeroed(arrayBuffer: ArrayBuffer, expectedSize: number): void {
+ this.expect(arrayBuffer.byteLength === expectedSize);
+ const view = new Uint8Array(arrayBuffer);
+ this.expectZero(view);
+ }
+
+ expectZero(actual: Uint8Array): void {
+ const size = actual.byteLength;
+ for (let i = 0; i < size; ++i) {
+ if (actual[i] !== 0) {
+ this.fail(`at [${i}], expected zero, got ${actual[i]}`);
+ break;
+ }
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/threading.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/threading.spec.ts
new file mode 100644
index 0000000000..b69404508d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/threading.spec.ts
@@ -0,0 +1,29 @@
+export const description = `
+Tests for valid operations with various client-side thread-shared state of GPUBuffers.
+
+States to test:
+- mapping pending
+- mapped
+- mapped at creation
+- mapped at creation, then unmapped
+- mapped at creation, then unmapped, then re-mapped
+- destroyed
+
+TODO: Look for more things to test.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('serialize')
+ .desc(
+ `Copy a GPUBuffer to another thread while it is in various states on
+{the sending thread, yet another thread}.`
+ )
+ .unimplemented();
+
+g.test('destroyed')
+ .desc(`Destroy on one thread while in various states in another thread.`)
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/basic.spec.ts
new file mode 100644
index 0000000000..06a15c4e31
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/basic.spec.ts
@@ -0,0 +1,98 @@
+export const description = `
+Basic tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { memcpy } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('empty').fn(async t => {
+ const encoder = t.device.createCommandEncoder();
+ const cmd = encoder.finish();
+ t.device.queue.submit([cmd]);
+});
+
+g.test('b2t2b').fn(async t => {
+ const data = new Uint32Array([0x01020304]);
+
+ const src = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ memcpy({ src: data }, { dst: src.getMappedRange() });
+ src.unmap();
+
+ const dst = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const mid = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'rgba8uint',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToTexture(
+ { buffer: src, bytesPerRow: 256 },
+ { texture: mid, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ encoder.copyTextureToBuffer(
+ { texture: mid, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dst, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(dst, data);
+});
+
+g.test('b2t2t2b').fn(async t => {
+ const data = new Uint32Array([0x01020304]);
+
+ const src = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ memcpy({ src: data }, { dst: src.getMappedRange() });
+ src.unmap();
+
+ const dst = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const midDesc: GPUTextureDescriptor = {
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'rgba8uint',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ };
+ const mid1 = t.device.createTexture(midDesc);
+ const mid2 = t.device.createTexture(midDesc);
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToTexture(
+ { buffer: src, bytesPerRow: 256 },
+ { texture: mid1, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ encoder.copyTextureToTexture(
+ { texture: mid1, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { texture: mid2, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ encoder.copyTextureToBuffer(
+ { texture: mid2, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dst, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(dst, data);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/clearBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/clearBuffer.spec.ts
new file mode 100644
index 0000000000..7fc3a6069e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/clearBuffer.spec.ts
@@ -0,0 +1,54 @@
+export const description = `
+API operations tests for clearBuffer.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('clear')
+ .desc(
+ `Validate the correctness of the clear by filling the srcBuffer with testable data, doing
+ clearBuffer(), and verifying the content of the whole srcBuffer with MapRead:
+ Clear {4 bytes, part of, the whole} buffer {with, without} a non-zero valid offset that
+ - covers the whole buffer
+ - covers the beginning of the buffer
+ - covers the end of the buffer
+ - covers neither the beginning nor the end of the buffer`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('offset', [0, 4, 8, 16, undefined])
+ .combine('size', [0, 4, 8, 16, undefined])
+ .expand('bufferSize', p => [
+ (p.offset ?? 0) + (p.size ?? 16),
+ (p.offset ?? 0) + (p.size ?? 16) + 8,
+ ])
+ )
+ .fn(async t => {
+ const { offset, size, bufferSize } = t.params;
+
+ const bufferData = new Uint8Array(bufferSize);
+ for (let i = 0; i < bufferSize; ++i) {
+ bufferData[i] = i + 1;
+ }
+
+ const buffer = t.makeBufferWithContents(
+ bufferData,
+ GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
+ );
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.clearBuffer(buffer, offset, size);
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectOffset = offset ?? 0;
+ const expectSize = size ?? bufferSize - expectOffset;
+
+ for (let i = 0; i < expectSize; ++i) {
+ bufferData[expectOffset + i] = 0;
+ }
+
+ t.expectGPUBufferValuesEqual(buffer, bufferData);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyBufferToBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyBufferToBuffer.spec.ts
new file mode 100644
index 0000000000..c7cae61d79
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyBufferToBuffer.spec.ts
@@ -0,0 +1,108 @@
+export const description = 'copyBufferToBuffer operation tests';
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('single')
+ .desc(
+ `Validate the correctness of the copy by filling the srcBuffer with testable data, doing
+ CopyBufferToBuffer() copy, and verifying the content of the whole dstBuffer with MapRead:
+ Copy {4 bytes, part of, the whole} srcBuffer to the dstBuffer {with, without} a non-zero valid
+ srcOffset that
+ - covers the whole dstBuffer
+ - covers the beginning of the dstBuffer
+ - covers the end of the dstBuffer
+ - covers neither the beginning nor the end of the dstBuffer`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcOffset', [0, 4, 8, 16])
+ .combine('dstOffset', [0, 4, 8, 16])
+ .combine('copySize', [0, 4, 8, 16])
+ .expand('srcBufferSize', p => [p.srcOffset + p.copySize, p.srcOffset + p.copySize + 8])
+ .expand('dstBufferSize', p => [p.dstOffset + p.copySize, p.dstOffset + p.copySize + 8])
+ )
+ .fn(async t => {
+ const { srcOffset, dstOffset, copySize, srcBufferSize, dstBufferSize } = t.params;
+
+ const srcData = new Uint8Array(srcBufferSize);
+ for (let i = 0; i < srcBufferSize; ++i) {
+ srcData[i] = i + 1;
+ }
+
+ const src = t.makeBufferWithContents(srcData, GPUBufferUsage.COPY_SRC);
+
+ const dst = t.device.createBuffer({
+ size: dstBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(dst);
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToBuffer(src, srcOffset, dst, dstOffset, copySize);
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectedDstData = new Uint8Array(dstBufferSize);
+ for (let i = 0; i < copySize; ++i) {
+ expectedDstData[dstOffset + i] = srcData[srcOffset + i];
+ }
+
+ t.expectGPUBufferValuesEqual(dst, expectedDstData);
+ });
+
+g.test('state_transitions')
+ .desc(
+ `Test proper state transitions/barriers happen between copy commands.
+ Copy part of src to dst, then a different part of dst to src, and check contents of both.`
+ )
+ .fn(async t => {
+ const srcData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ const dstData = new Uint8Array([10, 20, 30, 40, 50, 60, 70, 80]);
+
+ const src = t.makeBufferWithContents(
+ srcData,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+ const dst = t.makeBufferWithContents(
+ dstData,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToBuffer(src, 0, dst, 4, 4);
+ encoder.copyBufferToBuffer(dst, 0, src, 4, 4);
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectedSrcData = new Uint8Array([1, 2, 3, 4, 10, 20, 30, 40]);
+ const expectedDstData = new Uint8Array([10, 20, 30, 40, 1, 2, 3, 4]);
+ t.expectGPUBufferValuesEqual(src, expectedSrcData);
+ t.expectGPUBufferValuesEqual(dst, expectedDstData);
+ });
+
+g.test('copy_order')
+ .desc(
+ `Test copy commands in one command buffer occur in the correct order.
+ First copies one region from src to dst, then another region from src to an overlapping region
+ of dst, then checks the dst buffer's contents.`
+ )
+ .fn(async t => {
+ const srcData = new Uint32Array([1, 2, 3, 4, 5, 6, 7, 8]);
+
+ const src = t.makeBufferWithContents(srcData, GPUBufferUsage.COPY_SRC);
+
+ const dst = t.device.createBuffer({
+ size: srcData.length * 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(dst);
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToBuffer(src, 0, dst, 0, 16);
+ encoder.copyBufferToBuffer(src, 16, dst, 8, 16);
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectedDstData = new Uint32Array([1, 2, 5, 6, 7, 8, 0, 0]);
+ t.expectGPUBufferValuesEqual(dst, expectedDstData);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts
new file mode 100644
index 0000000000..4c1bff2302
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts
@@ -0,0 +1,1597 @@
+export const description = `copyTextureToTexture operation tests`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, memcpy, unreachable } from '../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ kRegularTextureFormats,
+ SizedTextureFormat,
+ kCompressedTextureFormats,
+ depthStencilFormatAspectSize,
+ DepthStencilFormat,
+ kBufferSizeAlignment,
+ kDepthStencilFormats,
+ kMinDynamicBufferOffsetAlignment,
+ kTextureDimensions,
+ textureDimensionAndFormatCompatible,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { makeBufferWithContents } from '../../../util/buffer.js';
+import { checkElementsEqual, checkElementsEqualEither } from '../../../util/check_contents.js';
+import { align } from '../../../util/math.js';
+import { physicalMipSize } from '../../../util/texture/base.js';
+import { DataArrayGenerator } from '../../../util/texture/data_generation.js';
+import { kBytesPerRowAlignment, dataBytesForCopyOrFail } from '../../../util/texture/layout.js';
+
+const dataGenerator = new DataArrayGenerator();
+
+class F extends GPUTest {
+ GetInitialDataPerMipLevel(
+ dimension: GPUTextureDimension,
+ textureSize: Required<GPUExtent3DDict>,
+ format: SizedTextureFormat,
+ mipLevel: number
+ ): Uint8Array {
+ const textureSizeAtLevel = physicalMipSize(textureSize, format, dimension, mipLevel);
+ const bytesPerBlock = kTextureFormatInfo[format].bytesPerBlock;
+ const blockWidthInTexel = kTextureFormatInfo[format].blockWidth;
+ const blockHeightInTexel = kTextureFormatInfo[format].blockHeight;
+ const blocksPerSubresource =
+ (textureSizeAtLevel.width / blockWidthInTexel) *
+ (textureSizeAtLevel.height / blockHeightInTexel);
+
+ const byteSize = bytesPerBlock * blocksPerSubresource * textureSizeAtLevel.depthOrArrayLayers;
+ return dataGenerator.generateView(byteSize);
+ }
+
+ GetInitialStencilDataPerMipLevel(
+ textureSize: Required<GPUExtent3DDict>,
+ format: DepthStencilFormat,
+ mipLevel: number
+ ): Uint8Array {
+ const textureSizeAtLevel = physicalMipSize(textureSize, format, '2d', mipLevel);
+ const aspectBytesPerBlock = depthStencilFormatAspectSize(format, 'stencil-only');
+ const byteSize =
+ aspectBytesPerBlock *
+ textureSizeAtLevel.width *
+ textureSizeAtLevel.height *
+ textureSizeAtLevel.depthOrArrayLayers;
+ return dataGenerator.generateView(byteSize);
+ }
+
+ DoCopyTextureToTextureTest(
+ dimension: GPUTextureDimension,
+ srcTextureSize: Required<GPUExtent3DDict>,
+ dstTextureSize: Required<GPUExtent3DDict>,
+ srcFormat: SizedTextureFormat,
+ dstFormat: SizedTextureFormat,
+ copyBoxOffsets: {
+ srcOffset: { x: number; y: number; z: number };
+ dstOffset: { x: number; y: number; z: number };
+ copyExtent: Required<GPUExtent3DDict>;
+ },
+ srcCopyLevel: number,
+ dstCopyLevel: number
+ ): void {
+ const mipLevelCount = dimension === '1d' ? 1 : 4;
+
+ // Create srcTexture and dstTexture
+ const srcTextureDesc: GPUTextureDescriptor = {
+ dimension,
+ size: srcTextureSize,
+ format: srcFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ mipLevelCount,
+ };
+ const srcTexture = this.device.createTexture(srcTextureDesc);
+ this.trackForCleanup(srcTexture);
+ const dstTextureDesc: GPUTextureDescriptor = {
+ dimension,
+ size: dstTextureSize,
+ format: dstFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ mipLevelCount,
+ };
+ const dstTexture = this.device.createTexture(dstTextureDesc);
+ this.trackForCleanup(dstTexture);
+
+ // Fill the whole subresource of srcTexture at srcCopyLevel with initialSrcData.
+ const initialSrcData = this.GetInitialDataPerMipLevel(
+ dimension,
+ srcTextureSize,
+ srcFormat,
+ srcCopyLevel
+ );
+ const srcTextureSizeAtLevel = physicalMipSize(
+ srcTextureSize,
+ srcFormat,
+ dimension,
+ srcCopyLevel
+ );
+ const bytesPerBlock = kTextureFormatInfo[srcFormat].bytesPerBlock;
+ const blockWidth = kTextureFormatInfo[srcFormat].blockWidth;
+ const blockHeight = kTextureFormatInfo[srcFormat].blockHeight;
+ const srcBlocksPerRow = srcTextureSizeAtLevel.width / blockWidth;
+ const srcBlockRowsPerImage = srcTextureSizeAtLevel.height / blockHeight;
+ this.device.queue.writeTexture(
+ { texture: srcTexture, mipLevel: srcCopyLevel },
+ initialSrcData,
+ {
+ bytesPerRow: srcBlocksPerRow * bytesPerBlock,
+ rowsPerImage: srcBlockRowsPerImage,
+ },
+ srcTextureSizeAtLevel
+ );
+
+ // Copy the region specified by copyBoxOffsets from srcTexture to dstTexture.
+ const dstTextureSizeAtLevel = physicalMipSize(
+ dstTextureSize,
+ dstFormat,
+ dimension,
+ dstCopyLevel
+ );
+ const minWidth = Math.min(srcTextureSizeAtLevel.width, dstTextureSizeAtLevel.width);
+ const minHeight = Math.min(srcTextureSizeAtLevel.height, dstTextureSizeAtLevel.height);
+ const minDepth = Math.min(
+ srcTextureSizeAtLevel.depthOrArrayLayers,
+ dstTextureSizeAtLevel.depthOrArrayLayers
+ );
+
+ const appliedSrcOffset = {
+ x: Math.min(copyBoxOffsets.srcOffset.x * blockWidth, minWidth),
+ y: Math.min(copyBoxOffsets.srcOffset.y * blockHeight, minHeight),
+ z: Math.min(copyBoxOffsets.srcOffset.z, minDepth),
+ };
+ const appliedDstOffset = {
+ x: Math.min(copyBoxOffsets.dstOffset.x * blockWidth, minWidth),
+ y: Math.min(copyBoxOffsets.dstOffset.y * blockHeight, minHeight),
+ z: Math.min(copyBoxOffsets.dstOffset.z, minDepth),
+ };
+
+ const appliedCopyWidth = Math.max(
+ minWidth +
+ copyBoxOffsets.copyExtent.width * blockWidth -
+ Math.max(appliedSrcOffset.x, appliedDstOffset.x),
+ 0
+ );
+ const appliedCopyHeight = Math.max(
+ minHeight +
+ copyBoxOffsets.copyExtent.height * blockHeight -
+ Math.max(appliedSrcOffset.y, appliedDstOffset.y),
+ 0
+ );
+ assert(appliedCopyWidth % blockWidth === 0 && appliedCopyHeight % blockHeight === 0);
+
+ const appliedCopyDepth = Math.max(
+ 0,
+ minDepth +
+ copyBoxOffsets.copyExtent.depthOrArrayLayers -
+ Math.max(appliedSrcOffset.z, appliedDstOffset.z)
+ );
+ assert(appliedCopyDepth >= 0);
+
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToTexture(
+ { texture: srcTexture, mipLevel: srcCopyLevel, origin: appliedSrcOffset },
+ { texture: dstTexture, mipLevel: dstCopyLevel, origin: appliedDstOffset },
+ { width: appliedCopyWidth, height: appliedCopyHeight, depthOrArrayLayers: appliedCopyDepth }
+ );
+
+ // Copy the whole content of dstTexture at dstCopyLevel to dstBuffer.
+ const dstBlocksPerRow = dstTextureSizeAtLevel.width / blockWidth;
+ const dstBlockRowsPerImage = dstTextureSizeAtLevel.height / blockHeight;
+ const bytesPerDstAlignedBlockRow = align(dstBlocksPerRow * bytesPerBlock, 256);
+ const dstBufferSize =
+ (dstBlockRowsPerImage * dstTextureSizeAtLevel.depthOrArrayLayers - 1) *
+ bytesPerDstAlignedBlockRow +
+ align(dstBlocksPerRow * bytesPerBlock, 4);
+ const dstBufferDesc: GPUBufferDescriptor = {
+ size: dstBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ };
+ const dstBuffer = this.device.createBuffer(dstBufferDesc);
+ this.trackForCleanup(dstBuffer);
+
+ encoder.copyTextureToBuffer(
+ { texture: dstTexture, mipLevel: dstCopyLevel },
+ {
+ buffer: dstBuffer,
+ bytesPerRow: bytesPerDstAlignedBlockRow,
+ rowsPerImage: dstBlockRowsPerImage,
+ },
+ dstTextureSizeAtLevel
+ );
+ this.device.queue.submit([encoder.finish()]);
+
+ // Fill expectedUint8DataWithPadding with the expected data of dstTexture. The other values in
+ // expectedUint8DataWithPadding are kept 0 to check if the texels untouched by the copy are 0
+ // (their previous values).
+ const expectedUint8DataWithPadding = new Uint8Array(dstBufferSize);
+ const expectedUint8Data = new Uint8Array(initialSrcData);
+
+ const appliedCopyBlocksPerRow = appliedCopyWidth / blockWidth;
+ const appliedCopyBlockRowsPerImage = appliedCopyHeight / blockHeight;
+ const srcCopyOffsetInBlocks = {
+ x: appliedSrcOffset.x / blockWidth,
+ y: appliedSrcOffset.y / blockHeight,
+ z: appliedSrcOffset.z,
+ };
+ const dstCopyOffsetInBlocks = {
+ x: appliedDstOffset.x / blockWidth,
+ y: appliedDstOffset.y / blockHeight,
+ z: appliedDstOffset.z,
+ };
+
+ for (let z = 0; z < appliedCopyDepth; ++z) {
+ const srcOffsetZ = srcCopyOffsetInBlocks.z + z;
+ const dstOffsetZ = dstCopyOffsetInBlocks.z + z;
+ for (let y = 0; y < appliedCopyBlockRowsPerImage; ++y) {
+ const dstOffsetYInBlocks = dstCopyOffsetInBlocks.y + y;
+ const expectedDataWithPaddingOffset =
+ bytesPerDstAlignedBlockRow * (dstBlockRowsPerImage * dstOffsetZ + dstOffsetYInBlocks) +
+ dstCopyOffsetInBlocks.x * bytesPerBlock;
+
+ const srcOffsetYInBlocks = srcCopyOffsetInBlocks.y + y;
+ const expectedDataOffset =
+ bytesPerBlock *
+ srcBlocksPerRow *
+ (srcBlockRowsPerImage * srcOffsetZ + srcOffsetYInBlocks) +
+ srcCopyOffsetInBlocks.x * bytesPerBlock;
+
+ const bytesInRow = appliedCopyBlocksPerRow * bytesPerBlock;
+ memcpy(
+ { src: expectedUint8Data, start: expectedDataOffset, length: bytesInRow },
+ { dst: expectedUint8DataWithPadding, start: expectedDataWithPaddingOffset }
+ );
+ }
+ }
+
+ let alternateExpectedData = expectedUint8DataWithPadding;
+ // For 8-byte snorm formats, allow an alternative encoding of -1.
+ // MAINTENANCE_TODO: Use textureContentIsOKByT2B with TexelView.
+ if (srcFormat.includes('snorm')) {
+ switch (srcFormat) {
+ case 'r8snorm':
+ case 'rg8snorm':
+ case 'rgba8snorm':
+ alternateExpectedData = alternateExpectedData.slice();
+ for (let i = 0; i < alternateExpectedData.length; ++i) {
+ if (alternateExpectedData[i] === 128) {
+ alternateExpectedData[i] = 129;
+ } else if (alternateExpectedData[i] === 129) {
+ alternateExpectedData[i] = 128;
+ }
+ }
+ break;
+ case 'bc4-r-snorm':
+ case 'bc5-rg-snorm':
+ case 'eac-r11snorm':
+ case 'eac-rg11snorm':
+ break;
+ default:
+ unreachable();
+ }
+ }
+
+ // Verify the content of the whole subresource of dstTexture at dstCopyLevel (in dstBuffer) is expected.
+ this.expectGPUBufferValuesPassCheck(
+ dstBuffer,
+ alternateExpectedData === expectedUint8DataWithPadding
+ ? vals => checkElementsEqual(vals, expectedUint8DataWithPadding)
+ : vals =>
+ checkElementsEqualEither(vals, [expectedUint8DataWithPadding, alternateExpectedData]),
+ {
+ srcByteOffset: 0,
+ type: Uint8Array,
+ typedLength: expectedUint8DataWithPadding.length,
+ }
+ );
+ }
+
+ InitializeStencilAspect(
+ sourceTexture: GPUTexture,
+ initialStencilData: Uint8Array,
+ srcCopyLevel: number,
+ srcCopyBaseArrayLayer: number,
+ copySize: readonly [number, number, number]
+ ): void {
+ this.queue.writeTexture(
+ {
+ texture: sourceTexture,
+ mipLevel: srcCopyLevel,
+ aspect: 'stencil-only',
+ origin: { x: 0, y: 0, z: srcCopyBaseArrayLayer },
+ },
+ initialStencilData,
+ { bytesPerRow: copySize[0], rowsPerImage: copySize[1] },
+ copySize
+ );
+ }
+
+ VerifyStencilAspect(
+ destinationTexture: GPUTexture,
+ initialStencilData: Uint8Array,
+ dstCopyLevel: number,
+ dstCopyBaseArrayLayer: number,
+ copySize: readonly [number, number, number]
+ ): void {
+ const bytesPerRow = align(copySize[0], kBytesPerRowAlignment);
+ const rowsPerImage = copySize[1];
+ const outputBufferSize = align(
+ dataBytesForCopyOrFail({
+ layout: { bytesPerRow, rowsPerImage },
+ format: 'stencil8',
+ copySize,
+ method: 'CopyT2B',
+ }),
+ kBufferSizeAlignment
+ );
+ const outputBuffer = this.device.createBuffer({
+ size: outputBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(outputBuffer);
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToBuffer(
+ {
+ texture: destinationTexture,
+ aspect: 'stencil-only',
+ mipLevel: dstCopyLevel,
+ origin: { x: 0, y: 0, z: dstCopyBaseArrayLayer },
+ },
+ { buffer: outputBuffer, bytesPerRow, rowsPerImage },
+ copySize
+ );
+ this.queue.submit([encoder.finish()]);
+
+ const expectedStencilData = new Uint8Array(outputBufferSize);
+ for (let z = 0; z < copySize[2]; ++z) {
+ const initialOffsetPerLayer = z * copySize[0] * copySize[1];
+ const expectedOffsetPerLayer = z * bytesPerRow * rowsPerImage;
+ for (let y = 0; y < copySize[1]; ++y) {
+ const initialOffsetPerRow = initialOffsetPerLayer + y * copySize[0];
+ const expectedOffsetPerRow = expectedOffsetPerLayer + y * bytesPerRow;
+ memcpy(
+ { src: initialStencilData, start: initialOffsetPerRow, length: copySize[0] },
+ { dst: expectedStencilData, start: expectedOffsetPerRow }
+ );
+ }
+ }
+ this.expectGPUBufferValuesEqual(outputBuffer, expectedStencilData);
+ }
+
+ GetRenderPipelineForT2TCopyWithDepthTests(
+ bindGroupLayout: GPUBindGroupLayout,
+ hasColorAttachment: boolean,
+ depthStencil: GPUDepthStencilState
+ ): GPURenderPipeline {
+ const renderPipelineDescriptor: GPURenderPipelineDescriptor = {
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Params {
+ copyLayer: f32
+ };
+ @group(0) @binding(0) var<uniform> param: Params;
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
+ var depthValue = 0.5 + 0.2 * sin(param.copyLayer);
+ var pos : array<vec3<f32>, 6> = array<vec3<f32>, 6>(
+ vec3<f32>(-1.0, 1.0, depthValue),
+ vec3<f32>(-1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, 1.0, 1.0),
+ vec3<f32>(-1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, 1.0, 1.0),
+ vec3<f32>( 1.0, -1.0, depthValue));
+ return vec4<f32>(pos[VertexIndex], 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ depthStencil,
+ };
+ if (hasColorAttachment) {
+ renderPipelineDescriptor.fragment = {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ };
+ }
+ return this.device.createRenderPipeline(renderPipelineDescriptor);
+ }
+
+ GetBindGroupLayoutForT2TCopyWithDepthTests(): GPUBindGroupLayout {
+ return this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.VERTEX,
+ buffer: {
+ type: 'uniform',
+ minBindingSize: 4,
+ hasDynamicOffset: true,
+ },
+ },
+ ],
+ });
+ }
+
+ GetBindGroupForT2TCopyWithDepthTests(
+ bindGroupLayout: GPUBindGroupLayout,
+ totalCopyArrayLayers: number
+ ): GPUBindGroup {
+ // Prepare the uniform buffer that contains all the copy layers to generate different depth
+ // values for different copy layers.
+ assert(totalCopyArrayLayers > 0);
+ const uniformBufferSize = kMinDynamicBufferOffsetAlignment * (totalCopyArrayLayers - 1) + 4;
+ const uniformBufferData = new Float32Array(uniformBufferSize / 4);
+ for (let i = 1; i < totalCopyArrayLayers; ++i) {
+ uniformBufferData[(kMinDynamicBufferOffsetAlignment / 4) * i] = i;
+ }
+ const uniformBuffer = makeBufferWithContents(
+ this.device,
+ uniformBufferData,
+ GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM
+ );
+ return this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ size: 4,
+ },
+ },
+ ],
+ });
+ }
+
+ /** Initialize the depth aspect of sourceTexture with draw calls */
+ InitializeDepthAspect(
+ sourceTexture: GPUTexture,
+ depthFormat: GPUTextureFormat,
+ srcCopyLevel: number,
+ srcCopyBaseArrayLayer: number,
+ copySize: readonly [number, number, number]
+ ): void {
+ // Prepare a renderPipeline with depthCompareFunction == 'always' and depthWriteEnabled == true
+ // for the initializations of the depth attachment.
+ const bindGroupLayout = this.GetBindGroupLayoutForT2TCopyWithDepthTests();
+ const renderPipeline = this.GetRenderPipelineForT2TCopyWithDepthTests(bindGroupLayout, false, {
+ format: depthFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'always',
+ });
+ const bindGroup = this.GetBindGroupForT2TCopyWithDepthTests(bindGroupLayout, copySize[2]);
+
+ const encoder = this.device.createCommandEncoder();
+ for (let srcCopyLayer = 0; srcCopyLayer < copySize[2]; ++srcCopyLayer) {
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: sourceTexture.createView({
+ baseArrayLayer: srcCopyLayer + srcCopyBaseArrayLayer,
+ arrayLayerCount: 1,
+ baseMipLevel: srcCopyLevel,
+ mipLevelCount: 1,
+ }),
+ depthClearValue: 0.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'store',
+ },
+ });
+ renderPass.setBindGroup(0, bindGroup, [srcCopyLayer * kMinDynamicBufferOffsetAlignment]);
+ renderPass.setPipeline(renderPipeline);
+ renderPass.draw(6);
+ renderPass.end();
+ }
+ this.queue.submit([encoder.finish()]);
+ }
+
+ VerifyDepthAspect(
+ destinationTexture: GPUTexture,
+ depthFormat: GPUTextureFormat,
+ dstCopyLevel: number,
+ dstCopyBaseArrayLayer: number,
+ copySize: [number, number, number]
+ ): void {
+ // Prepare a renderPipeline with depthCompareFunction == 'equal' and depthWriteEnabled == false
+ // for the comparison of the depth attachment.
+ const bindGroupLayout = this.GetBindGroupLayoutForT2TCopyWithDepthTests();
+ const renderPipeline = this.GetRenderPipelineForT2TCopyWithDepthTests(bindGroupLayout, true, {
+ format: depthFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'equal',
+ });
+ const bindGroup = this.GetBindGroupForT2TCopyWithDepthTests(bindGroupLayout, copySize[2]);
+
+ const outputColorTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: copySize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ this.trackForCleanup(outputColorTexture);
+ const encoder = this.device.createCommandEncoder();
+ for (let dstCopyLayer = 0; dstCopyLayer < copySize[2]; ++dstCopyLayer) {
+ // If the depth value is not expected, the color of outputColorTexture will remain Red after
+ // the render pass.
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputColorTexture.createView({
+ baseArrayLayer: dstCopyLayer,
+ arrayLayerCount: 1,
+ }),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: destinationTexture.createView({
+ baseArrayLayer: dstCopyLayer + dstCopyBaseArrayLayer,
+ arrayLayerCount: 1,
+ baseMipLevel: dstCopyLevel,
+ mipLevelCount: 1,
+ }),
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'store',
+ },
+ });
+ renderPass.setBindGroup(0, bindGroup, [dstCopyLayer * kMinDynamicBufferOffsetAlignment]);
+ renderPass.setPipeline(renderPipeline);
+ renderPass.draw(6);
+ renderPass.end();
+ }
+ this.queue.submit([encoder.finish()]);
+
+ this.expectSingleColor(outputColorTexture, 'rgba8unorm', {
+ size: copySize,
+ exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ });
+ }
+}
+
+const kCopyBoxOffsetsForWholeDepth = [
+ // From (0, 0) of src to (0, 0) of dst.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (0, 0) of src to (blockWidth, 0) of dst.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 1, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (0, 0) of src to (0, blockHeight) of dst.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 1, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (blockWidth, 0) of src to (0, 0) of dst.
+ {
+ srcOffset: { x: 1, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (0, blockHeight) of src to (0, 0) of dst.
+ {
+ srcOffset: { x: 0, y: 1, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (blockWidth, 0) of src to (0, 0) of dst, and the copy extent will not cover the last
+ // texel block column of both source and destination texture.
+ {
+ srcOffset: { x: 1, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: -1, height: 0, depthOrArrayLayers: 0 },
+ },
+ // From (0, blockHeight) of src to (0, 0) of dst, and the copy extent will not cover the last
+ // texel block row of both source and destination texture.
+ {
+ srcOffset: { x: 0, y: 1, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: -1, depthOrArrayLayers: 0 },
+ },
+] as const;
+
+const kCopyBoxOffsetsFor2DArrayTextures = [
+ // Copy the whole array slices from the source texture to the destination texture.
+ // The copy extent will cover the whole subresource of either source or the
+ // destination texture
+ ...kCopyBoxOffsetsForWholeDepth,
+
+ // Copy 1 texture slice from the 1st slice of the source texture to the 1st slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -2 },
+ },
+ // Copy 1 texture slice from the 2nd slice of the source texture to the 2nd slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 1 },
+ dstOffset: { x: 0, y: 0, z: 1 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -3 },
+ },
+ // Copy 1 texture slice from the 1st slice of the source texture to the 2nd slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 1 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 },
+ },
+ // Copy 1 texture slice from the 2nd slice of the source texture to the 1st slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 1 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 },
+ },
+ // Copy 2 texture slices from the 1st slice of the source texture to the 1st slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -3 },
+ },
+ // Copy 3 texture slices from the 2nd slice of the source texture to the 2nd slice of the
+ // destination texture.
+ {
+ srcOffset: { x: 0, y: 0, z: 1 },
+ dstOffset: { x: 0, y: 0, z: 1 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 },
+ },
+] as const;
+
+export const g = makeTestGroup(F);
+
+g.test('color_textures,non_compressed,non_array')
+ .desc(
+ `
+ Validate the correctness of the copy by filling the srcTexture with testable data and any
+ non-compressed color format supported by WebGPU, doing CopyTextureToTexture() copy, and verifying
+ the content of the whole dstTexture.
+
+ Copy {1 texel block, part of, the whole} srcTexture to the dstTexture {with, without} a non-zero
+ valid srcOffset that
+ - covers the whole dstTexture subresource
+ - covers the corners of the dstTexture
+ - doesn't cover any texels that are on the edge of the dstTexture
+ - covers the mipmap level > 0
+
+ Tests for all pairs of valid source/destination formats, and all texture dimensions.
+ `
+ )
+ .params(u =>
+ u
+ .combine('srcFormat', kRegularTextureFormats)
+ .combine('dstFormat', kRegularTextureFormats)
+ .filter(({ srcFormat, dstFormat }) => {
+ const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat;
+ const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat;
+ return (
+ srcFormat === dstFormat ||
+ (srcBaseFormat !== undefined &&
+ dstBaseFormat !== undefined &&
+ srcBaseFormat === dstBaseFormat)
+ );
+ })
+ .combine('dimension', kTextureDimensions)
+ .filter(
+ ({ dimension, srcFormat, dstFormat }) =>
+ textureDimensionAndFormatCompatible(dimension, srcFormat) &&
+ textureDimensionAndFormatCompatible(dimension, dstFormat)
+ )
+ .beginSubcases()
+ .expandWithParams(p => {
+ const params = [
+ {
+ srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 },
+ dstTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 },
+ },
+ {
+ srcTextureSize: { width: 31, height: 33, depthOrArrayLayers: 1 },
+ dstTextureSize: { width: 31, height: 33, depthOrArrayLayers: 1 },
+ },
+ {
+ srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 },
+ dstTextureSize: { width: 64, height: 64, depthOrArrayLayers: 1 },
+ },
+ {
+ srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 },
+ dstTextureSize: { width: 63, height: 61, depthOrArrayLayers: 1 },
+ },
+ ];
+ if (p.dimension === '1d') {
+ for (const param of params) {
+ param.srcTextureSize.height = 1;
+ param.dstTextureSize.height = 1;
+ }
+ }
+
+ return params;
+ })
+ .combine('copyBoxOffsets', kCopyBoxOffsetsForWholeDepth)
+ .unless(
+ p =>
+ p.dimension === '1d' &&
+ (p.copyBoxOffsets.copyExtent.height !== 0 ||
+ p.copyBoxOffsets.srcOffset.y !== 0 ||
+ p.copyBoxOffsets.dstOffset.y !== 0)
+ )
+ .combine('srcCopyLevel', [0, 3])
+ .combine('dstCopyLevel', [0, 3])
+ .unless(p => p.dimension === '1d' && (p.srcCopyLevel !== 0 || p.dstCopyLevel !== 0))
+ )
+ .fn(async t => {
+ const {
+ dimension,
+ srcTextureSize,
+ dstTextureSize,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel,
+ } = t.params;
+
+ t.DoCopyTextureToTextureTest(
+ dimension,
+ srcTextureSize,
+ dstTextureSize,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel
+ );
+ });
+
+g.test('color_textures,compressed,non_array')
+ .desc(
+ `
+ Validate the correctness of the copy by filling the srcTexture with testable data and any
+ compressed color format supported by WebGPU, doing CopyTextureToTexture() copy, and verifying
+ the content of the whole dstTexture.
+
+ Tests for all pairs of valid source/destination formats, and all texture dimensions.
+ `
+ )
+ .params(u =>
+ u
+ .combine('srcFormat', kCompressedTextureFormats)
+ .combine('dstFormat', kCompressedTextureFormats)
+ .filter(({ srcFormat, dstFormat }) => {
+ const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat;
+ const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat;
+ return (
+ srcFormat === dstFormat ||
+ (srcBaseFormat !== undefined &&
+ dstBaseFormat !== undefined &&
+ srcBaseFormat === dstBaseFormat)
+ );
+ })
+ .combine('dimension', kTextureDimensions)
+ .filter(
+ ({ dimension, srcFormat, dstFormat }) =>
+ textureDimensionAndFormatCompatible(dimension, srcFormat) &&
+ textureDimensionAndFormatCompatible(dimension, dstFormat)
+ )
+ .beginSubcases()
+ .combine('textureSizeInBlocks', [
+ // The heights and widths in blocks are all power of 2
+ { src: { width: 16, height: 8 }, dst: { width: 16, height: 8 } },
+ // The virtual width of the source texture at mipmap level 2 (15) is not a multiple of 4 blocks
+ { src: { width: 15, height: 8 }, dst: { width: 16, height: 8 } },
+ // The virtual width of the destination texture at mipmap level 2 (15) is not a multiple
+ // of 4 blocks
+ { src: { width: 16, height: 8 }, dst: { width: 15, height: 8 } },
+ // The virtual height of the source texture at mipmap level 2 (13) is not a multiple of 4 blocks
+ { src: { width: 16, height: 13 }, dst: { width: 16, height: 8 } },
+ // The virtual height of the destination texture at mipmap level 2 (13) is not a
+ // multiple of 4 blocks
+ { src: { width: 16, height: 8 }, dst: { width: 16, height: 13 } },
+ // None of the widths or heights in blocks are power of 2
+ { src: { width: 15, height: 13 }, dst: { width: 15, height: 13 } },
+ ])
+ .combine('copyBoxOffsets', kCopyBoxOffsetsForWholeDepth)
+ .combine('srcCopyLevel', [0, 2])
+ .combine('dstCopyLevel', [0, 2])
+ )
+ .beforeAllSubcases(t => {
+ const { srcFormat, dstFormat } = t.params;
+ t.selectDeviceOrSkipTestCase([
+ kTextureFormatInfo[srcFormat].feature,
+ kTextureFormatInfo[dstFormat].feature,
+ ]);
+ })
+ .fn(async t => {
+ const {
+ dimension,
+ textureSizeInBlocks,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel,
+ } = t.params;
+ const srcBlockWidth = kTextureFormatInfo[srcFormat].blockWidth;
+ const srcBlockHeight = kTextureFormatInfo[srcFormat].blockHeight;
+ const dstBlockWidth = kTextureFormatInfo[dstFormat].blockWidth;
+ const dstBlockHeight = kTextureFormatInfo[dstFormat].blockHeight;
+
+ t.DoCopyTextureToTextureTest(
+ dimension,
+ {
+ width: textureSizeInBlocks.src.width * srcBlockWidth,
+ height: textureSizeInBlocks.src.height * srcBlockHeight,
+ depthOrArrayLayers: 1,
+ },
+ {
+ width: textureSizeInBlocks.dst.width * dstBlockWidth,
+ height: textureSizeInBlocks.dst.height * dstBlockHeight,
+ depthOrArrayLayers: 1,
+ },
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel
+ );
+ });
+
+g.test('color_textures,non_compressed,array')
+ .desc(
+ `
+ Validate the correctness of the texture-to-texture copy on 2D array textures by filling the
+ srcTexture with testable data and any non-compressed color format supported by WebGPU, doing
+ CopyTextureToTexture() copy, and verifying the content of the whole dstTexture.
+ `
+ )
+ .params(u =>
+ u
+ .combine('srcFormat', kRegularTextureFormats)
+ .combine('dstFormat', kRegularTextureFormats)
+ .filter(({ srcFormat, dstFormat }) => {
+ const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat;
+ const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat;
+ return (
+ srcFormat === dstFormat ||
+ (srcBaseFormat !== undefined &&
+ dstBaseFormat !== undefined &&
+ srcBaseFormat === dstBaseFormat)
+ );
+ })
+ .combine('dimension', ['2d', '3d'] as const)
+ .filter(
+ ({ dimension, srcFormat, dstFormat }) =>
+ textureDimensionAndFormatCompatible(dimension, srcFormat) &&
+ textureDimensionAndFormatCompatible(dimension, dstFormat)
+ )
+ .beginSubcases()
+ .combine('textureSize', [
+ {
+ srcTextureSize: { width: 64, height: 32, depthOrArrayLayers: 5 },
+ dstTextureSize: { width: 64, height: 32, depthOrArrayLayers: 5 },
+ },
+ {
+ srcTextureSize: { width: 31, height: 33, depthOrArrayLayers: 5 },
+ dstTextureSize: { width: 31, height: 33, depthOrArrayLayers: 5 },
+ },
+ {
+ srcTextureSize: { width: 31, height: 32, depthOrArrayLayers: 33 },
+ dstTextureSize: { width: 31, height: 32, depthOrArrayLayers: 33 },
+ },
+ ])
+
+ .combine('copyBoxOffsets', kCopyBoxOffsetsFor2DArrayTextures)
+ .combine('srcCopyLevel', [0, 3])
+ .combine('dstCopyLevel', [0, 3])
+ )
+ .fn(async t => {
+ const {
+ dimension,
+ textureSize,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel,
+ } = t.params;
+
+ t.DoCopyTextureToTextureTest(
+ dimension,
+ textureSize.srcTextureSize,
+ textureSize.dstTextureSize,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel
+ );
+ });
+
+g.test('color_textures,compressed,array')
+ .desc(
+ `
+ Validate the correctness of the texture-to-texture copy on 2D array textures by filling the
+ srcTexture with testable data and any compressed color format supported by WebGPU, doing
+ CopyTextureToTexture() copy, and verifying the content of the whole dstTexture.
+
+ Tests for all pairs of valid source/destination formats, and all texture dimensions.
+ `
+ )
+ .params(u =>
+ u
+ .combine('srcFormat', kCompressedTextureFormats)
+ .combine('dstFormat', kCompressedTextureFormats)
+ .filter(({ srcFormat, dstFormat }) => {
+ const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat;
+ const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat;
+ return (
+ srcFormat === dstFormat ||
+ (srcBaseFormat !== undefined &&
+ dstBaseFormat !== undefined &&
+ srcBaseFormat === dstBaseFormat)
+ );
+ })
+ .combine('dimension', ['2d', '3d'] as const)
+ .filter(
+ ({ dimension, srcFormat, dstFormat }) =>
+ textureDimensionAndFormatCompatible(dimension, srcFormat) &&
+ textureDimensionAndFormatCompatible(dimension, dstFormat)
+ )
+ .beginSubcases()
+ .combine('textureSizeInBlocks', [
+ // The heights and widths in blocks are all power of 2
+ { src: { width: 2, height: 2 }, dst: { width: 2, height: 2 } },
+ // None of the widths or heights in blocks are power of 2
+ { src: { width: 15, height: 13 }, dst: { width: 15, height: 13 } },
+ ])
+ .combine('copyBoxOffsets', kCopyBoxOffsetsFor2DArrayTextures)
+ .combine('srcCopyLevel', [0, 2])
+ .combine('dstCopyLevel', [0, 2])
+ )
+ .beforeAllSubcases(t => {
+ const { srcFormat, dstFormat } = t.params;
+
+ t.selectDeviceOrSkipTestCase([
+ kTextureFormatInfo[srcFormat].feature,
+ kTextureFormatInfo[dstFormat].feature,
+ ]);
+ })
+ .fn(async t => {
+ const {
+ dimension,
+ textureSizeInBlocks,
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel,
+ } = t.params;
+ const srcBlockWidth = kTextureFormatInfo[srcFormat].blockWidth;
+ const srcBlockHeight = kTextureFormatInfo[srcFormat].blockHeight;
+ const dstBlockWidth = kTextureFormatInfo[dstFormat].blockWidth;
+ const dstBlockHeight = kTextureFormatInfo[dstFormat].blockHeight;
+
+ t.DoCopyTextureToTextureTest(
+ dimension,
+ {
+ width: textureSizeInBlocks.src.width * srcBlockWidth,
+ height: textureSizeInBlocks.src.height * srcBlockHeight,
+ depthOrArrayLayers: 5,
+ },
+ {
+ width: textureSizeInBlocks.dst.width * dstBlockWidth,
+ height: textureSizeInBlocks.dst.height * dstBlockHeight,
+ depthOrArrayLayers: 5,
+ },
+ srcFormat,
+ dstFormat,
+ copyBoxOffsets,
+ srcCopyLevel,
+ dstCopyLevel
+ );
+ });
+
+g.test('zero_sized')
+ .desc(
+ `
+ Validate the correctness of zero-sized copies (should be no-ops).
+
+ - For each texture dimension.
+ - Copies that are zero-sized in only one dimension {x, y, z}, each touching the {lower, upper} end
+ of that dimension.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([
+ { dimension: '1d', textureSize: { width: 32, height: 1, depthOrArrayLayers: 1 } },
+ { dimension: '2d', textureSize: { width: 32, height: 32, depthOrArrayLayers: 5 } },
+ { dimension: '3d', textureSize: { width: 32, height: 32, depthOrArrayLayers: 5 } },
+ ] as const)
+ .combine('copyBoxOffset', [
+ // copyExtent.width === 0
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.width === 0 && srcOffset.x === textureWidth
+ {
+ srcOffset: { x: 64, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.width === 0 && dstOffset.x === textureWidth
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 64, y: 0, z: 0 },
+ copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.height === 0
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.height === 0 && srcOffset.y === textureHeight
+ {
+ srcOffset: { x: 0, y: 32, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.height === 0 && dstOffset.y === textureHeight
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 32, z: 0 },
+ copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.depthOrArrayLayers === 0
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: -5 },
+ },
+ // copyExtent.depthOrArrayLayers === 0 && srcOffset.z === textureDepth
+ {
+ srcOffset: { x: 0, y: 0, z: 5 },
+ dstOffset: { x: 0, y: 0, z: 0 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ // copyExtent.depthOrArrayLayers === 0 && dstOffset.z === textureDepth
+ {
+ srcOffset: { x: 0, y: 0, z: 0 },
+ dstOffset: { x: 0, y: 0, z: 5 },
+ copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ },
+ ])
+ .unless(
+ p =>
+ p.dimension === '1d' &&
+ (p.copyBoxOffset.copyExtent.height !== 0 ||
+ p.copyBoxOffset.srcOffset.y !== 0 ||
+ p.copyBoxOffset.dstOffset.y !== 0)
+ )
+ .combine('srcCopyLevel', [0, 3])
+ .combine('dstCopyLevel', [0, 3])
+ .unless(p => p.dimension === '1d' && (p.srcCopyLevel !== 0 || p.dstCopyLevel !== 0))
+ )
+ .fn(async t => {
+ const { dimension, textureSize, copyBoxOffset, srcCopyLevel, dstCopyLevel } = t.params;
+
+ const srcFormat = 'rgba8unorm';
+ const dstFormat = 'rgba8unorm';
+
+ t.DoCopyTextureToTextureTest(
+ dimension,
+ textureSize,
+ textureSize,
+ srcFormat,
+ dstFormat,
+ copyBoxOffset,
+ srcCopyLevel,
+ dstCopyLevel
+ );
+ });
+
+g.test('copy_depth_stencil')
+ .desc(
+ `
+ Validate the correctness of copyTextureToTexture() with depth and stencil aspect.
+
+ For all the texture formats with stencil aspect:
+ - Initialize the stencil aspect of the source texture with writeTexture().
+ - Copy the stencil aspect from the source texture into the destination texture
+ - Copy the stencil aspect of the destination texture into another staging buffer and check its
+ content
+ - Test the copies from / into zero / non-zero array layer / mipmap levels
+ - Test copying multiple array layers
+
+ For all the texture formats with depth aspect:
+ - Initialize the depth aspect of the source texture with a draw call
+ - Copy the depth aspect from the source texture into the destination texture
+ - Validate the content in the destination texture with the depth comparison function 'equal'
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('srcTextureSize', [
+ { width: 32, height: 16, depthOrArrayLayers: 1 },
+ { width: 32, height: 16, depthOrArrayLayers: 4 },
+ { width: 24, height: 48, depthOrArrayLayers: 5 },
+ ])
+ .combine('srcCopyLevel', [0, 2])
+ .combine('dstCopyLevel', [0, 2])
+ .combine('srcCopyBaseArrayLayer', [0, 1])
+ .combine('dstCopyBaseArrayLayer', [0, 1])
+ .filter(t => {
+ return (
+ t.srcTextureSize.depthOrArrayLayers > t.srcCopyBaseArrayLayer &&
+ t.srcTextureSize.depthOrArrayLayers > t.dstCopyBaseArrayLayer
+ );
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const {
+ format,
+ srcTextureSize,
+ srcCopyLevel,
+ dstCopyLevel,
+ srcCopyBaseArrayLayer,
+ dstCopyBaseArrayLayer,
+ } = t.params;
+
+ const copySize: [number, number, number] = [
+ srcTextureSize.width >> srcCopyLevel,
+ srcTextureSize.height >> srcCopyLevel,
+ srcTextureSize.depthOrArrayLayers - Math.max(srcCopyBaseArrayLayer, dstCopyBaseArrayLayer),
+ ];
+ const sourceTexture = t.device.createTexture({
+ format,
+ size: srcTextureSize,
+ usage:
+ GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ mipLevelCount: srcCopyLevel + 1,
+ });
+ t.trackForCleanup(sourceTexture);
+ const destinationTexture = t.device.createTexture({
+ format,
+ size: [
+ copySize[0] << dstCopyLevel,
+ copySize[1] << dstCopyLevel,
+ srcTextureSize.depthOrArrayLayers,
+ ] as const,
+ usage:
+ GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ mipLevelCount: dstCopyLevel + 1,
+ });
+ t.trackForCleanup(destinationTexture);
+
+ let initialStencilData: undefined | Uint8Array = undefined;
+ if (kTextureFormatInfo[format].stencil) {
+ initialStencilData = t.GetInitialStencilDataPerMipLevel(srcTextureSize, format, srcCopyLevel);
+ t.InitializeStencilAspect(
+ sourceTexture,
+ initialStencilData,
+ srcCopyLevel,
+ srcCopyBaseArrayLayer,
+ copySize
+ );
+ }
+ if (kTextureFormatInfo[format].depth) {
+ t.InitializeDepthAspect(sourceTexture, format, srcCopyLevel, srcCopyBaseArrayLayer, copySize);
+ }
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyTextureToTexture(
+ {
+ texture: sourceTexture,
+ mipLevel: srcCopyLevel,
+ origin: { x: 0, y: 0, z: srcCopyBaseArrayLayer },
+ },
+ {
+ texture: destinationTexture,
+ mipLevel: dstCopyLevel,
+ origin: { x: 0, y: 0, z: dstCopyBaseArrayLayer },
+ },
+ copySize
+ );
+ t.queue.submit([encoder.finish()]);
+
+ if (kTextureFormatInfo[format].stencil) {
+ assert(initialStencilData !== undefined);
+ t.VerifyStencilAspect(
+ destinationTexture,
+ initialStencilData,
+ dstCopyLevel,
+ dstCopyBaseArrayLayer,
+ copySize
+ );
+ }
+ if (kTextureFormatInfo[format].depth) {
+ t.VerifyDepthAspect(
+ destinationTexture,
+ format,
+ dstCopyLevel,
+ dstCopyBaseArrayLayer,
+ copySize
+ );
+ }
+ });
+
+g.test('copy_multisampled_color')
+ .desc(
+ `
+ Validate the correctness of copyTextureToTexture() with multisampled color formats.
+
+ - Initialize the source texture with a triangle in a render pass.
+ - Copy from the source texture into the destination texture with CopyTextureToTexture().
+ - Compare every sub-pixel of source texture and destination texture in another render pass:
+ - If they are different, then output RED; otherwise output GREEN
+ - Verify the pixels in the output texture are all GREEN.
+ - Note that in current WebGPU SPEC the mipmap level count and array layer count of a multisampled
+ texture can only be 1.
+ `
+ )
+ .fn(async t => {
+ const textureSize = [32, 16, 1] as const;
+ const kColorFormat = 'rgba8unorm';
+ const kSampleCount = 4;
+
+ const sourceTexture = t.device.createTexture({
+ format: kColorFormat,
+ size: textureSize,
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: kSampleCount,
+ });
+ t.trackForCleanup(sourceTexture);
+ const destinationTexture = t.device.createTexture({
+ format: kColorFormat,
+ size: textureSize,
+ usage:
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: kSampleCount,
+ });
+ t.trackForCleanup(destinationTexture);
+
+ // Initialize sourceTexture with a draw call.
+ const renderPipelineForInit = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0)
+ );
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.3, 0.5, 0.8, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: kColorFormat }],
+ },
+ multisample: {
+ count: kSampleCount,
+ },
+ });
+ const initEncoder = t.device.createCommandEncoder();
+ const renderPassForInit = initEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: sourceTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPassForInit.setPipeline(renderPipelineForInit);
+ renderPassForInit.draw(3);
+ renderPassForInit.end();
+ t.queue.submit([initEncoder.finish()]);
+
+ // Do the texture-to-texture copy
+ const copyEncoder = t.device.createCommandEncoder();
+ copyEncoder.copyTextureToTexture(
+ {
+ texture: sourceTexture,
+ },
+ {
+ texture: destinationTexture,
+ },
+ textureSize
+ );
+ t.queue.submit([copyEncoder.finish()]);
+
+ // Verify if all the sub-pixel values at the same location of sourceTexture and
+ // destinationTexture are equal.
+ const renderPipelineForValidation = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var sourceTexture : texture_multisampled_2d<f32>;
+ @group(0) @binding(1) var destinationTexture : texture_multisampled_2d<f32>;
+ @fragment
+ fn main(@builtin(position) coord_in: vec4<f32>) -> @location(0) vec4<f32> {
+ var coord_in_vec2 = vec2<i32>(i32(coord_in.x), i32(coord_in.y));
+ for (var sampleIndex = 0; sampleIndex < ${kSampleCount};
+ sampleIndex = sampleIndex + 1) {
+ var sourceSubPixel : vec4<f32> =
+ textureLoad(sourceTexture, coord_in_vec2, sampleIndex);
+ var destinationSubPixel : vec4<f32> =
+ textureLoad(destinationTexture, coord_in_vec2, sampleIndex);
+ if (!all(sourceSubPixel == destinationSubPixel)) {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ }
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: kColorFormat }],
+ },
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: renderPipelineForValidation.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: sourceTexture.createView(),
+ },
+ {
+ binding: 1,
+ resource: destinationTexture.createView(),
+ },
+ ],
+ });
+ const expectedOutputTexture = t.device.createTexture({
+ format: kColorFormat,
+ size: textureSize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(expectedOutputTexture);
+ const validationEncoder = t.device.createCommandEncoder();
+ const renderPassForValidation = validationEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: expectedOutputTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPassForValidation.setPipeline(renderPipelineForValidation);
+ renderPassForValidation.setBindGroup(0, bindGroup);
+ renderPassForValidation.draw(6);
+ renderPassForValidation.end();
+ t.queue.submit([validationEncoder.finish()]);
+
+ t.expectSingleColor(expectedOutputTexture, 'rgba8unorm', {
+ size: [textureSize[0], textureSize[1], textureSize[2]],
+ exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ });
+ });
+
+g.test('copy_multisampled_depth')
+ .desc(
+ `
+ Validate the correctness of copyTextureToTexture() with multisampled depth formats.
+
+ - Initialize the source texture with a triangle in a render pass.
+ - Copy from the source texture into the destination texture with CopyTextureToTexture().
+ - Validate the content in the destination texture with the depth comparison function 'equal'.
+ - Note that in current WebGPU SPEC the mipmap level count and array layer count of a multisampled
+ texture can only be 1.
+ `
+ )
+ .fn(async t => {
+ const textureSize = [32, 16, 1] as const;
+ const kDepthFormat = 'depth24plus';
+ const kSampleCount = 4;
+
+ const sourceTexture = t.device.createTexture({
+ format: kDepthFormat,
+ size: textureSize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: kSampleCount,
+ });
+ t.trackForCleanup(sourceTexture);
+ const destinationTexture = t.device.createTexture({
+ format: kDepthFormat,
+ size: textureSize,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: kSampleCount,
+ });
+ t.trackForCleanup(destinationTexture);
+
+ const vertexState: GPUVertexState = {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
+ var pos : array<vec3<f32>, 6> = array<vec3<f32>, 6>(
+ vec3<f32>(-1.0, 1.0, 0.5),
+ vec3<f32>(-1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, 1.0, 1.0),
+ vec3<f32>(-1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, 1.0, 1.0),
+ vec3<f32>( 1.0, -1.0, 0.5));
+ return vec4<f32>(pos[VertexIndex], 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ };
+
+ // Initialize the depth aspect of source texture with a draw call
+ const renderPipelineForInit = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: vertexState,
+ depthStencil: {
+ format: kDepthFormat,
+ depthCompare: 'always',
+ depthWriteEnabled: true,
+ },
+ multisample: {
+ count: kSampleCount,
+ },
+ });
+
+ const encoderForInit = t.device.createCommandEncoder();
+ const renderPassForInit = encoderForInit.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: sourceTexture.createView(),
+ depthClearValue: 0.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ },
+ });
+ renderPassForInit.setPipeline(renderPipelineForInit);
+ renderPassForInit.draw(6);
+ renderPassForInit.end();
+ t.queue.submit([encoderForInit.finish()]);
+
+ // Do the texture-to-texture copy
+ const copyEncoder = t.device.createCommandEncoder();
+ copyEncoder.copyTextureToTexture(
+ {
+ texture: sourceTexture,
+ },
+ {
+ texture: destinationTexture,
+ },
+ textureSize
+ );
+ t.queue.submit([copyEncoder.finish()]);
+
+ // Verify the depth values in destinationTexture are what we expected with
+ // depthCompareFunction == 'equal' and depthWriteEnabled == false in the render pipeline
+ const kColorFormat = 'rgba8unorm';
+ const renderPipelineForVerify = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: vertexState,
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: kColorFormat }],
+ },
+ depthStencil: {
+ format: kDepthFormat,
+ depthCompare: 'equal',
+ depthWriteEnabled: false,
+ },
+ multisample: {
+ count: kSampleCount,
+ },
+ });
+ const multisampledColorTexture = t.device.createTexture({
+ format: kColorFormat,
+ size: textureSize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: kSampleCount,
+ });
+ t.trackForCleanup(multisampledColorTexture);
+ const colorTextureAsResolveTarget = t.device.createTexture({
+ format: kColorFormat,
+ size: textureSize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(colorTextureAsResolveTarget);
+
+ const encoderForVerify = t.device.createCommandEncoder();
+ const renderPassForVerify = encoderForVerify.beginRenderPass({
+ colorAttachments: [
+ {
+ view: multisampledColorTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'discard',
+ resolveTarget: colorTextureAsResolveTarget.createView(),
+ },
+ ],
+ depthStencilAttachment: {
+ view: destinationTexture.createView(),
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ },
+ });
+ renderPassForVerify.setPipeline(renderPipelineForVerify);
+ renderPassForVerify.draw(6);
+ renderPassForVerify.end();
+ t.queue.submit([encoderForVerify.finish()]);
+
+ t.expectSingleColor(colorTextureAsResolveTarget, kColorFormat, {
+ size: [textureSize[0], textureSize[1], textureSize[2]],
+ exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts
new file mode 100644
index 0000000000..4713cf8c60
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/image_copy.spec.ts
@@ -0,0 +1,1983 @@
+export const description = `writeTexture + copyBufferToTexture + copyTextureToBuffer operation tests.
+
+* copy_with_various_rows_per_image_and_bytes_per_row: test that copying data with various bytesPerRow (including { ==, > } bytesInACompleteRow) and\
+ rowsPerImage (including { ==, > } copyExtent.height) values and minimum required bytes in copy works for every format. Also covers special code paths:
+ - bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
+ - when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1: copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
+ - bytesPerRow == bytesInACompleteCopyImage
+
+* copy_with_various_offsets_and_data_sizes: test that copying data with various offset (including { ==, > } 0 and is/isn't power of 2) values and additional\
+ data paddings works for every format with 2d and 2d-array textures. Also covers special code paths:
+ - offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
+ - offset > bytesInACompleteCopyImage
+
+* copy_with_various_origins_and_copy_extents: test that copying slices of a texture works with various origin (including { origin.x, origin.y, origin.z }\
+ { ==, > } 0 and is/isn't power of 2) and copyExtent (including { copyExtent.x, copyExtent.y, copyExtent.z } { ==, > } 0 and is/isn't power of 2) values\
+ (also including {origin._ + copyExtent._ { ==, < } the subresource size of textureCopyView) works for all formats. origin and copyExtent values are passed\
+ as [number, number, number] instead of GPUExtent3DDict.
+
+* copy_various_mip_levels: test that copying various mip levels works for all formats. Also covers special code paths:
+ - the physical size of the subresource is not equal to the logical size
+ - bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers and copyExtent needs to be clamped
+
+* copy_with_no_image_or_slice_padding_and_undefined_values: test that when copying a single row we can set any bytesPerRow value and when copying a single\
+ slice we can set rowsPerImage to 0. Also test setting offset, rowsPerImage, mipLevel, origin, origin.{x,y,z} to undefined.
+
+* TODO:
+ - add another initMethod which renders the texture [3]
+ - test copyT2B with buffer size not divisible by 4 (not done because expectContents 4-byte alignment)
+ - Convert the float32 values in initialData into the ones compatible to the depth aspect of
+ depthFormats when depth16unorm is supported by the browsers in
+ DoCopyTextureToBufferWithDepthAspectTest().
+
+TODO: Expand tests of GPUExtent3D [1]
+
+TODO: Fix this test for the various skipped formats [2]:
+- snorm tests failing due to rounding
+- float tests failing because float values are not byte-preserved
+- compressed formats
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, memcpy, TypedArrayBufferView, unreachable } from '../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ SizedTextureFormat,
+ kDepthStencilFormats,
+ kMinDynamicBufferOffsetAlignment,
+ kBufferSizeAlignment,
+ DepthStencilFormat,
+ depthStencilBufferTextureCopySupported,
+ depthStencilFormatAspectSize,
+ kTextureDimensions,
+ textureDimensionAndFormatCompatible,
+ kColorTextureFormats,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { makeBufferWithContents } from '../../../util/buffer.js';
+import { align } from '../../../util/math.js';
+import { DataArrayGenerator } from '../../../util/texture/data_generation.js';
+import {
+ bytesInACompleteRow,
+ dataBytesForCopyOrFail,
+ getTextureCopyLayout,
+ kBytesPerRowAlignment,
+ TextureCopyLayout,
+} from '../../../util/texture/layout.js';
+
+interface TextureCopyViewWithRequiredOrigin {
+ texture: GPUTexture;
+ mipLevel: number | undefined;
+ origin: Required<GPUOrigin3DDict>;
+}
+
+/** Describes the function used to copy the initial data into the texture. */
+type InitMethod = 'WriteTexture' | 'CopyB2T';
+/**
+ * - PartialCopyT2B: do CopyT2B to check that the part of the texture we copied to with InitMethod
+ * matches the data we were copying and that we don't overwrite any data in the target buffer that
+ * we're not supposed to - that's primarily for testing CopyT2B functionality.
+ * - FullCopyT2B: do CopyT2B on the whole texture and check wether the part we copied to matches
+ * the data we were copying and that the nothing else was modified - that's primarily for testing
+ * WriteTexture and CopyB2T.
+ */
+type CheckMethod = 'PartialCopyT2B' | 'FullCopyT2B';
+
+/**
+ * This describes in what form the arguments will be passed to WriteTexture/CopyB2T/CopyT2B. If
+ * undefined, then default values are passed as undefined instead of default values. If arrays, then
+ * `GPUOrigin3D` and `GPUExtent3D` are passed as `[number, number, number]`. *
+ *
+ * [1]: Try to expand this with something like:
+ * ```ts
+ * function encodeExtent3D(
+ * mode: 'partial-array' | 'full-array' | 'extra-array' | 'partial-dict' | 'full-dict',
+ * value: GPUExtent3D
+ * ): GPUExtent3D { ... }
+ * ```
+ */
+type ChangeBeforePass = 'none' | 'undefined' | 'arrays';
+
+/** Each combination of methods assume that the ones before it were tested and work correctly. */
+const kMethodsToTest = [
+ // Then we make sure that WriteTexture works for all formats:
+ { initMethod: 'WriteTexture', checkMethod: 'FullCopyT2B' },
+ // Then we make sure that CopyB2T works for all formats:
+ { initMethod: 'CopyB2T', checkMethod: 'FullCopyT2B' },
+ // Then we make sure that CopyT2B works for all formats:
+ { initMethod: 'WriteTexture', checkMethod: 'PartialCopyT2B' },
+] as const;
+
+// [2]: Fix things so this list can be reduced to zero (see file description)
+const kExcludedFormats: Set<SizedTextureFormat> = new Set([
+ 'r8snorm',
+ 'rg8snorm',
+ 'rgba8snorm',
+ 'rg11b10ufloat',
+ 'rg16float',
+ 'rgba16float',
+ 'r32float',
+ 'rg32float',
+ 'rgba32float',
+]);
+const kWorkingColorTextureFormats = kColorTextureFormats.filter(x => !kExcludedFormats.has(x));
+
+const dataGenerator = new DataArrayGenerator();
+const altDataGenerator = new DataArrayGenerator();
+
+class ImageCopyTest extends GPUTest {
+ /** Offset for a particular texel in the linear texture data */
+ getTexelOffsetInBytes(
+ textureDataLayout: Required<GPUImageDataLayout>,
+ format: SizedTextureFormat,
+ texel: Required<GPUOrigin3DDict>,
+ origin: Required<GPUOrigin3DDict> = { x: 0, y: 0, z: 0 }
+ ): number {
+ const { offset, bytesPerRow, rowsPerImage } = textureDataLayout;
+ const info = kTextureFormatInfo[format];
+
+ assert(texel.x >= origin.x && texel.y >= origin.y && texel.z >= origin.z);
+ assert(texel.x % info.blockWidth === 0);
+ assert(texel.y % info.blockHeight === 0);
+ assert(origin.x % info.blockWidth === 0);
+ assert(origin.y % info.blockHeight === 0);
+
+ const bytesPerImage = rowsPerImage * bytesPerRow;
+
+ return (
+ offset +
+ (texel.z - origin.z) * bytesPerImage +
+ ((texel.y - origin.y) / info.blockHeight) * bytesPerRow +
+ ((texel.x - origin.x) / info.blockWidth) * info.bytesPerBlock
+ );
+ }
+
+ *iterateBlockRows(
+ size: Required<GPUExtent3DDict>,
+ origin: Required<GPUOrigin3DDict>,
+ format: SizedTextureFormat
+ ): Generator<Required<GPUOrigin3DDict>> {
+ if (size.width === 0 || size.height === 0 || size.depthOrArrayLayers === 0) {
+ // do not iterate anything for an empty region
+ return;
+ }
+ const info = kTextureFormatInfo[format];
+ assert(size.height % info.blockHeight === 0);
+ for (let y = 0; y < size.height; y += info.blockHeight) {
+ for (let z = 0; z < size.depthOrArrayLayers; ++z) {
+ yield {
+ x: origin.x,
+ y: origin.y + y,
+ z: origin.z + z,
+ };
+ }
+ }
+ }
+
+ /**
+ * This is used for testing passing undefined members of `GPUImageDataLayout` instead of actual
+ * values where possible. Passing arguments as values and not as objects so that they are passed
+ * by copy and not by reference.
+ */
+ undefDataLayoutIfNeeded(
+ offset: number | undefined,
+ rowsPerImage: number | undefined,
+ bytesPerRow: number | undefined,
+ changeBeforePass: ChangeBeforePass
+ ): GPUImageDataLayout {
+ if (changeBeforePass === 'undefined') {
+ if (offset === 0) {
+ offset = undefined;
+ }
+ if (bytesPerRow === 0) {
+ bytesPerRow = undefined;
+ }
+ if (rowsPerImage === 0) {
+ rowsPerImage = undefined;
+ }
+ }
+ return { offset, bytesPerRow, rowsPerImage };
+ }
+
+ /**
+ * This is used for testing passing undefined members of `GPUImageCopyTexture` instead of actual
+ * values where possible and also for testing passing the origin as `[number, number, number]`.
+ * Passing arguments as values and not as objects so that they are passed by copy and not by
+ * reference.
+ */
+ undefOrArrayCopyViewIfNeeded(
+ texture: GPUTexture,
+ origin_x: number | undefined,
+ origin_y: number | undefined,
+ origin_z: number | undefined,
+ mipLevel: number | undefined,
+ changeBeforePass: ChangeBeforePass
+ ): GPUImageCopyTexture {
+ let origin: GPUOrigin3D | undefined = { x: origin_x, y: origin_y, z: origin_z };
+
+ if (changeBeforePass === 'undefined') {
+ if (origin_x === 0 && origin_y === 0 && origin_z === 0) {
+ origin = undefined;
+ } else {
+ if (origin_x === 0) {
+ origin_x = undefined;
+ }
+ if (origin_y === 0) {
+ origin_y = undefined;
+ }
+ if (origin_z === 0) {
+ origin_z = undefined;
+ }
+ origin = { x: origin_x, y: origin_y, z: origin_z };
+ }
+
+ if (mipLevel === 0) {
+ mipLevel = undefined;
+ }
+ }
+
+ if (changeBeforePass === 'arrays') {
+ origin = [origin_x!, origin_y!, origin_z!];
+ }
+
+ return { texture, origin, mipLevel };
+ }
+
+ /**
+ * This is used for testing passing `GPUExtent3D` as `[number, number, number]` instead of
+ * `GPUExtent3DDict`. Passing arguments as values and not as objects so that they are passed by
+ * copy and not by reference.
+ */
+ arrayCopySizeIfNeeded(
+ width: number,
+ height: number,
+ depthOrArrayLayers: number,
+ changeBeforePass: ChangeBeforePass
+ ): GPUExtent3D {
+ if (changeBeforePass === 'arrays') {
+ return [width, height, depthOrArrayLayers];
+ } else {
+ return { width, height, depthOrArrayLayers };
+ }
+ }
+
+ /** Run a CopyT2B command with appropriate arguments corresponding to `ChangeBeforePass` */
+ copyTextureToBufferWithAppliedArguments(
+ buffer: GPUBuffer,
+ { offset, rowsPerImage, bytesPerRow }: Required<GPUImageDataLayout>,
+ { width, height, depthOrArrayLayers }: Required<GPUExtent3DDict>,
+ { texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
+ changeBeforePass: ChangeBeforePass
+ ): void {
+ const { x, y, z } = origin;
+
+ const appliedCopyView = this.undefOrArrayCopyViewIfNeeded(
+ texture,
+ x,
+ y,
+ z,
+ mipLevel,
+ changeBeforePass
+ );
+ const appliedDataLayout = this.undefDataLayoutIfNeeded(
+ offset,
+ rowsPerImage,
+ bytesPerRow,
+ changeBeforePass
+ );
+ const appliedCheckSize = this.arrayCopySizeIfNeeded(
+ width,
+ height,
+ depthOrArrayLayers,
+ changeBeforePass
+ );
+
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToBuffer(
+ appliedCopyView,
+ { buffer, ...appliedDataLayout },
+ appliedCheckSize
+ );
+ this.device.queue.submit([encoder.finish()]);
+ }
+
+ /** Put data into a part of the texture with an appropriate method. */
+ uploadLinearTextureDataToTextureSubBox(
+ textureCopyView: TextureCopyViewWithRequiredOrigin,
+ textureDataLayout: GPUImageDataLayout & { bytesPerRow: number },
+ copySize: Required<GPUExtent3DDict>,
+ partialData: Uint8Array,
+ method: InitMethod,
+ changeBeforePass: ChangeBeforePass
+ ): void {
+ const { texture, mipLevel, origin } = textureCopyView;
+ const { offset, rowsPerImage, bytesPerRow } = textureDataLayout;
+ const { x, y, z } = origin;
+ const { width, height, depthOrArrayLayers } = copySize;
+
+ const appliedCopyView = this.undefOrArrayCopyViewIfNeeded(
+ texture,
+ x,
+ y,
+ z,
+ mipLevel,
+ changeBeforePass
+ );
+ const appliedDataLayout = this.undefDataLayoutIfNeeded(
+ offset,
+ rowsPerImage,
+ bytesPerRow,
+ changeBeforePass
+ );
+ const appliedCopySize = this.arrayCopySizeIfNeeded(
+ width,
+ height,
+ depthOrArrayLayers,
+ changeBeforePass
+ );
+
+ switch (method) {
+ case 'WriteTexture': {
+ this.device.queue.writeTexture(
+ appliedCopyView,
+ partialData,
+ appliedDataLayout,
+ appliedCopySize
+ );
+
+ break;
+ }
+ case 'CopyB2T': {
+ const buffer = this.makeBufferWithContents(partialData, GPUBufferUsage.COPY_SRC);
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyBufferToTexture(
+ { buffer, ...appliedDataLayout },
+ appliedCopyView,
+ appliedCopySize
+ );
+ this.device.queue.submit([encoder.finish()]);
+
+ break;
+ }
+ default:
+ unreachable();
+ }
+ }
+
+ /**
+ * We check an appropriate part of the texture against the given data.
+ * Used directly with PartialCopyT2B check method (for a subpart of the texture)
+ * and by `copyWholeTextureToBufferAndCheckContentsWithUpdatedData` with FullCopyT2B check method
+ * (for the whole texture). We also ensure that CopyT2B doesn't overwrite bytes it's not supposed
+ * to if validateOtherBytesInBuffer is set to true.
+ */
+ copyPartialTextureToBufferAndCheckContents(
+ { texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
+ checkSize: Required<GPUExtent3DDict>,
+ format: SizedTextureFormat,
+ expected: Uint8Array,
+ expectedDataLayout: Required<GPUImageDataLayout>,
+ changeBeforePass: ChangeBeforePass = 'none'
+ ): void {
+ // The alignment is necessary because we need to copy and map data from this buffer.
+ const bufferSize = align(expected.byteLength, 4);
+ // The start value ensures generated data here doesn't match the expected data.
+ const bufferData = altDataGenerator.generateAndCopyView(bufferSize, 17);
+
+ const buffer = this.makeBufferWithContents(
+ bufferData,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+
+ this.copyTextureToBufferWithAppliedArguments(
+ buffer,
+ expectedDataLayout,
+ checkSize,
+ { texture, mipLevel, origin },
+ changeBeforePass
+ );
+
+ this.updateLinearTextureDataSubBox(
+ expectedDataLayout,
+ expectedDataLayout,
+ checkSize,
+ origin,
+ origin,
+ format,
+ bufferData,
+ expected
+ );
+
+ this.expectGPUBufferValuesEqual(buffer, bufferData);
+ }
+
+ /**
+ * Copies the whole texture into linear data stored in a buffer for further checks.
+ *
+ * Used for `copyWholeTextureToBufferAndCheckContentsWithUpdatedData`.
+ */
+ copyWholeTextureToNewBuffer(
+ { texture, mipLevel }: { texture: GPUTexture; mipLevel: number | undefined },
+ resultDataLayout: TextureCopyLayout
+ ): GPUBuffer {
+ const { mipSize, byteLength, bytesPerRow, rowsPerImage } = resultDataLayout;
+ const buffer = this.device.createBuffer({
+ size: align(byteLength, 4), // this is necessary because we need to copy and map data from this buffer
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(buffer);
+
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToBuffer(
+ { texture, mipLevel },
+ { buffer, bytesPerRow, rowsPerImage },
+ mipSize
+ );
+ this.device.queue.submit([encoder.finish()]);
+
+ return buffer;
+ }
+
+ /**
+ * Takes the data returned by `copyWholeTextureToNewBuffer` and updates it after a copy operation
+ * on the texture by emulating the copy behaviour here directly.
+ */
+ updateLinearTextureDataSubBox(
+ destinationDataLayout: Required<GPUImageDataLayout>,
+ sourceDataLayout: Required<GPUImageDataLayout>,
+ copySize: Required<GPUExtent3DDict>,
+ destinationOrigin: Required<GPUOrigin3DDict>,
+ sourceOrigin: Required<GPUOrigin3DDict>,
+ format: SizedTextureFormat,
+ destination: Uint8Array,
+ source: Uint8Array
+ ): void {
+ for (const texel of this.iterateBlockRows(copySize, sourceOrigin, format)) {
+ const srcOffsetElements = this.getTexelOffsetInBytes(
+ sourceDataLayout,
+ format,
+ texel,
+ sourceOrigin
+ );
+ const dstOffsetElements = this.getTexelOffsetInBytes(
+ destinationDataLayout,
+ format,
+ texel,
+ destinationOrigin
+ );
+ const rowLength = bytesInACompleteRow(copySize.width, format);
+ memcpy(
+ { src: source, start: srcOffsetElements, length: rowLength },
+ { dst: destination, start: dstOffsetElements }
+ );
+ }
+ }
+
+ /**
+ * Used for checking whether the whole texture was updated correctly by
+ * `uploadLinearTextureDataToTextureSubpart`. Takes fullData returned by
+ * `copyWholeTextureToNewBuffer` before the copy operation which is the original texture data,
+ * then updates it with `updateLinearTextureDataSubpart` and checks the texture against the
+ * updated data after the copy operation.
+ */
+ copyWholeTextureToBufferAndCheckContentsWithUpdatedData(
+ { texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
+ fullTextureCopyLayout: TextureCopyLayout,
+ texturePartialDataLayout: Required<GPUImageDataLayout>,
+ copySize: Required<GPUExtent3DDict>,
+ format: SizedTextureFormat,
+ fullData: GPUBuffer,
+ partialData: Uint8Array
+ ): void {
+ const { mipSize, bytesPerRow, rowsPerImage, byteLength } = fullTextureCopyLayout;
+ const readbackPromise = this.readGPUBufferRangeTyped(fullData, {
+ type: Uint8Array,
+ typedLength: byteLength,
+ });
+
+ const destinationOrigin = { x: 0, y: 0, z: 0 };
+
+ // We add an eventual async expectation which will update the full data and then add
+ // other eventual async expectations to ensure it will be correct.
+ this.eventualAsyncExpectation(async () => {
+ const readback = await readbackPromise;
+ this.updateLinearTextureDataSubBox(
+ { offset: 0, ...fullTextureCopyLayout },
+ texturePartialDataLayout,
+ copySize,
+ destinationOrigin,
+ origin,
+ format,
+ readback.data,
+ partialData
+ );
+ this.copyPartialTextureToBufferAndCheckContents(
+ { texture, mipLevel, origin: destinationOrigin },
+ { width: mipSize[0], height: mipSize[1], depthOrArrayLayers: mipSize[2] },
+ format,
+ readback.data,
+ { bytesPerRow, rowsPerImage, offset: 0 }
+ );
+ readback.cleanup();
+ });
+ }
+
+ /**
+ * Tests copy between linear data and texture by creating a texture, putting some data into it
+ * with WriteTexture/CopyB2T, then getting data for the whole texture/for a part of it back and
+ * comparing it with the expectation.
+ */
+ uploadTextureAndVerifyCopy({
+ textureDataLayout,
+ copySize,
+ dataSize,
+ mipLevel = 0,
+ origin = { x: 0, y: 0, z: 0 },
+ textureSize,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ changeBeforePass = 'none',
+ }: {
+ textureDataLayout: Required<GPUImageDataLayout>;
+ copySize: Required<GPUExtent3DDict>;
+ dataSize: number;
+ mipLevel?: number;
+ origin?: Required<GPUOrigin3DDict>;
+ textureSize: readonly [number, number, number];
+ format: SizedTextureFormat;
+ dimension: GPUTextureDimension;
+ initMethod: InitMethod;
+ checkMethod: CheckMethod;
+ changeBeforePass?: ChangeBeforePass;
+ }): void {
+ const texture = this.device.createTexture({
+ size: textureSize as [number, number, number],
+ format,
+ dimension,
+ mipLevelCount: mipLevel + 1,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+ this.trackForCleanup(texture);
+
+ const data = dataGenerator.generateView(dataSize);
+
+ switch (checkMethod) {
+ case 'PartialCopyT2B': {
+ this.uploadLinearTextureDataToTextureSubBox(
+ { texture, mipLevel, origin },
+ textureDataLayout,
+ copySize,
+ data,
+ initMethod,
+ changeBeforePass
+ );
+
+ this.copyPartialTextureToBufferAndCheckContents(
+ { texture, mipLevel, origin },
+ copySize,
+ format,
+ data,
+ textureDataLayout,
+ changeBeforePass
+ );
+
+ break;
+ }
+ case 'FullCopyT2B': {
+ const fullTextureCopyLayout = getTextureCopyLayout(format, dimension, textureSize, {
+ mipLevel,
+ });
+
+ const fullData = this.copyWholeTextureToNewBuffer(
+ { texture, mipLevel },
+ fullTextureCopyLayout
+ );
+
+ this.uploadLinearTextureDataToTextureSubBox(
+ { texture, mipLevel, origin },
+ textureDataLayout,
+ copySize,
+ data,
+ initMethod,
+ changeBeforePass
+ );
+
+ this.copyWholeTextureToBufferAndCheckContentsWithUpdatedData(
+ { texture, mipLevel, origin },
+ fullTextureCopyLayout,
+ textureDataLayout,
+ copySize,
+ format,
+ fullData,
+ data
+ );
+
+ break;
+ }
+ default:
+ unreachable();
+ }
+ }
+
+ async DoUploadToStencilTest(
+ format: DepthStencilFormat,
+ textureSize: readonly [number, number, number],
+ uploadMethod: 'WriteTexture' | 'CopyB2T',
+ bytesPerRow: number,
+ rowsPerImage: number,
+ initialDataSize: number,
+ initialDataOffset: number,
+ mipLevel: number
+ ): Promise<void> {
+ const srcTexture = this.device.createTexture({
+ size: textureSize,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ format,
+ mipLevelCount: mipLevel + 1,
+ });
+ this.trackForCleanup(srcTexture);
+
+ const copySize = [textureSize[0] >> mipLevel, textureSize[1] >> mipLevel, textureSize[2]];
+ const initialData = dataGenerator.generateView(
+ align(initialDataSize, kBufferSizeAlignment),
+ 0,
+ initialDataOffset
+ );
+ switch (uploadMethod) {
+ case 'WriteTexture':
+ this.queue.writeTexture(
+ { texture: srcTexture, aspect: 'stencil-only', mipLevel },
+ initialData,
+ {
+ offset: initialDataOffset,
+ bytesPerRow,
+ rowsPerImage,
+ },
+ copySize
+ );
+ break;
+ case 'CopyB2T':
+ {
+ const stagingBuffer = makeBufferWithContents(
+ this.device,
+ initialData,
+ GPUBufferUsage.COPY_SRC
+ );
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyBufferToTexture(
+ { buffer: stagingBuffer, offset: initialDataOffset, bytesPerRow, rowsPerImage },
+ { texture: srcTexture, aspect: 'stencil-only', mipLevel },
+ copySize
+ );
+ this.queue.submit([encoder.finish()]);
+ }
+ break;
+ default:
+ unreachable();
+ }
+
+ await this.checkStencilTextureContent(
+ srcTexture,
+ textureSize,
+ format,
+ initialData,
+ initialDataOffset,
+ bytesPerRow,
+ rowsPerImage,
+ mipLevel
+ );
+ }
+
+ async DoCopyFromStencilTest(
+ format: DepthStencilFormat,
+ textureSize: readonly [number, number, number],
+ bytesPerRow: number,
+ rowsPerImage: number,
+ offset: number,
+ mipLevel: number
+ ): Promise<void> {
+ const srcTexture = this.device.createTexture({
+ size: textureSize,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ format,
+ mipLevelCount: mipLevel + 1,
+ });
+ this.trackForCleanup(srcTexture);
+
+ // Initialize srcTexture with queue.writeTexture()
+ const copySize = [textureSize[0] >> mipLevel, textureSize[1] >> mipLevel, textureSize[2]];
+ const initialData = dataGenerator.generateView(
+ align(copySize[0] * copySize[1] * copySize[2], kBufferSizeAlignment)
+ );
+ this.queue.writeTexture(
+ { texture: srcTexture, aspect: 'stencil-only', mipLevel },
+ initialData,
+ { bytesPerRow: copySize[0], rowsPerImage: copySize[1] },
+ copySize
+ );
+
+ // Copy the stencil aspect from srcTexture into outputBuffer.
+ const outputBufferSize = align(
+ offset +
+ dataBytesForCopyOrFail({
+ layout: { bytesPerRow, rowsPerImage },
+ format: 'stencil8',
+ copySize,
+ method: 'CopyT2B',
+ }),
+ kBufferSizeAlignment
+ );
+ const outputBuffer = this.device.createBuffer({
+ size: outputBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(outputBuffer);
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToBuffer(
+ { texture: srcTexture, aspect: 'stencil-only', mipLevel },
+ { buffer: outputBuffer, offset, bytesPerRow, rowsPerImage },
+ copySize
+ );
+ this.queue.submit([encoder.finish()]);
+
+ // Validate the data in outputBuffer is what we expect.
+ const expectedData = new Uint8Array(outputBufferSize);
+ for (let z = 0; z < copySize[2]; ++z) {
+ const baseExpectedOffset = offset + z * bytesPerRow * rowsPerImage;
+ const baseInitialDataOffset = z * copySize[0] * copySize[1];
+ for (let y = 0; y < copySize[1]; ++y) {
+ memcpy(
+ {
+ src: initialData,
+ start: baseInitialDataOffset + y * copySize[0],
+ length: copySize[0],
+ },
+ { dst: expectedData, start: baseExpectedOffset + y * bytesPerRow }
+ );
+ }
+ }
+ this.expectGPUBufferValuesEqual(outputBuffer, expectedData);
+ }
+
+ // MAINTENANCE_TODO(#881): Migrate this into the texture_ok helpers.
+ async checkStencilTextureContent(
+ stencilTexture: GPUTexture,
+ stencilTextureSize: readonly [number, number, number],
+ stencilTextureFormat: GPUTextureFormat,
+ expectedStencilTextureData: Uint8Array,
+ expectedStencilTextureDataOffset: number,
+ expectedStencilTextureDataBytesPerRow: number,
+ expectedStencilTextureDataRowsPerImage: number,
+ stencilTextureMipLevel: number
+ ): Promise<void> {
+ const stencilBitCount = 8;
+
+ // Prepare the uniform buffer that stores the bit indices (from 0 to 7) at stride 256 (required
+ // by Dynamic Buffer Offset).
+ const uniformBufferSize = kMinDynamicBufferOffsetAlignment * (stencilBitCount - 1) + 4;
+ const uniformBufferData = new Uint32Array(uniformBufferSize / 4);
+ for (let i = 1; i < stencilBitCount; ++i) {
+ uniformBufferData[(kMinDynamicBufferOffsetAlignment / 4) * i] = i;
+ }
+ const uniformBuffer = makeBufferWithContents(
+ this.device,
+ uniformBufferData,
+ GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM
+ );
+
+ // Prepare the base render pipeline descriptor (all the settings expect stencilReadMask).
+ const bindGroupLayout = this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'uniform',
+ minBindingSize: 4,
+ hasDynamicOffset: true,
+ },
+ },
+ ],
+ });
+ const renderPipelineDescriptorBase: GPURenderPipelineDescriptor = {
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Params {
+ stencilBitIndex: u32
+ };
+ @group(0) @binding(0) var<uniform> param: Params;
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(f32(1u << param.stencilBitIndex) / 255.0, 0.0, 0.0, 0.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ // As we implement "rendering one bit in each draw() call" with blending operation
+ // 'add', the format of outputTexture must support blending.
+ format: 'r8unorm',
+ blend: {
+ color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
+ alpha: {},
+ },
+ },
+ ],
+ },
+
+ primitive: {
+ topology: 'triangle-list',
+ },
+
+ depthStencil: {
+ format: stencilTextureFormat,
+ stencilFront: {
+ compare: 'equal',
+ },
+ stencilBack: {
+ compare: 'equal',
+ },
+ },
+ };
+
+ // Prepare the bindGroup that contains uniformBuffer and referenceTexture.
+ const bindGroup = this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ size: 4,
+ },
+ },
+ ],
+ });
+
+ // "Copy" the stencil value into the color attachment with 8 draws in one render pass. Each draw
+ // will "Copy" one bit of the stencil value into the color attachment. The bit of the stencil
+ // value is specified by setStencilReference().
+ const copyFromOutputTextureLayout = getTextureCopyLayout(
+ stencilTextureFormat,
+ '2d',
+ [stencilTextureSize[0], stencilTextureSize[1], 1],
+ {
+ mipLevel: stencilTextureMipLevel,
+ aspect: 'stencil-only',
+ }
+ );
+ const outputTextureSize = [
+ copyFromOutputTextureLayout.mipSize[0],
+ copyFromOutputTextureLayout.mipSize[1],
+ 1,
+ ];
+ const outputTexture = this.device.createTexture({
+ format: 'r8unorm',
+ size: outputTextureSize,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ this.trackForCleanup(outputTexture);
+
+ for (
+ let stencilTextureLayer = 0;
+ stencilTextureLayer < stencilTextureSize[2];
+ ++stencilTextureLayer
+ ) {
+ const encoder = this.device.createCommandEncoder();
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: stencilTexture.createView({
+ baseMipLevel: stencilTextureMipLevel,
+ mipLevelCount: 1,
+ baseArrayLayer: stencilTextureLayer,
+ arrayLayerCount: 1,
+ }),
+ };
+ if (kTextureFormatInfo[stencilTextureFormat].depth) {
+ depthStencilAttachment.depthClearValue = 0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'store';
+ }
+ if (kTextureFormatInfo[stencilTextureFormat].stencil) {
+ depthStencilAttachment.stencilLoadOp = 'load';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment,
+ });
+
+ for (let stencilBitIndex = 0; stencilBitIndex < stencilBitCount; ++stencilBitIndex) {
+ const renderPipelineDescriptor = renderPipelineDescriptorBase;
+ assert(renderPipelineDescriptor.depthStencil !== undefined);
+ renderPipelineDescriptor.depthStencil.stencilReadMask = 1 << stencilBitIndex;
+ const renderPipeline = this.device.createRenderPipeline(renderPipelineDescriptor);
+
+ renderPass.setPipeline(renderPipeline);
+ renderPass.setStencilReference(1 << stencilBitIndex);
+ renderPass.setBindGroup(0, bindGroup, [stencilBitIndex * kMinDynamicBufferOffsetAlignment]);
+ renderPass.draw(6);
+ }
+ renderPass.end();
+
+ // Check outputTexture by copying the content of outputTexture into outputStagingBuffer and
+ // checking all the data in outputStagingBuffer.
+ const outputStagingBuffer = this.device.createBuffer({
+ size: copyFromOutputTextureLayout.byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(outputStagingBuffer);
+ encoder.copyTextureToBuffer(
+ {
+ texture: outputTexture,
+ },
+ {
+ buffer: outputStagingBuffer,
+ bytesPerRow: copyFromOutputTextureLayout.bytesPerRow,
+ rowsPerImage: copyFromOutputTextureLayout.rowsPerImage,
+ },
+ outputTextureSize
+ );
+
+ this.queue.submit([encoder.finish()]);
+
+ // Check the valid data in outputStagingBuffer once per row.
+ for (let y = 0; y < copyFromOutputTextureLayout.mipSize[1]; ++y) {
+ this.expectGPUBufferValuesEqual(
+ outputStagingBuffer,
+ expectedStencilTextureData.slice(
+ expectedStencilTextureDataOffset +
+ expectedStencilTextureDataBytesPerRow *
+ expectedStencilTextureDataRowsPerImage *
+ stencilTextureLayer +
+ expectedStencilTextureDataBytesPerRow * y,
+ copyFromOutputTextureLayout.mipSize[0]
+ )
+ );
+ }
+ }
+ }
+
+ // MAINTENANCE_TODO(#881): Consider if this can be simplified/encapsulated using TexelView.
+ initializeDepthAspectWithRendering(
+ depthTexture: GPUTexture,
+ depthFormat: GPUTextureFormat,
+ copySize: readonly [number, number, number],
+ copyMipLevel: number,
+ initialData: Float32Array
+ ): void {
+ assert(kTextureFormatInfo[depthFormat].depth);
+
+ const inputTexture = this.device.createTexture({
+ size: copySize,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
+ format: 'r32float',
+ });
+ this.trackForCleanup(inputTexture);
+ this.queue.writeTexture(
+ { texture: inputTexture },
+ initialData,
+ {
+ bytesPerRow: copySize[0] * 4,
+ rowsPerImage: copySize[1],
+ },
+ copySize
+ );
+
+ const renderPipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
+ @fragment fn main(@builtin(position) fragcoord : vec4<f32>) ->
+ @builtin(frag_depth) f32 {
+ var depthValue : vec4<f32> = textureLoad(inputTexture, vec2<i32>(fragcoord.xy), 0);
+ return depthValue.x;
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ depthStencil: {
+ format: depthFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'always',
+ },
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ for (let z = 0; z < copySize[2]; ++z) {
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthTexture.createView({
+ dimension: '2d',
+ baseArrayLayer: z,
+ arrayLayerCount: 1,
+ baseMipLevel: copyMipLevel,
+ mipLevelCount: 1,
+ }),
+ };
+ if (kTextureFormatInfo[depthFormat].depth) {
+ depthStencilAttachment.depthClearValue = 0.0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'store';
+ }
+ if (kTextureFormatInfo[depthFormat].stencil) {
+ depthStencilAttachment.stencilLoadOp = 'load';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment,
+ });
+ renderPass.setPipeline(renderPipeline);
+
+ const bindGroup = this.device.createBindGroup({
+ layout: renderPipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: inputTexture.createView({
+ dimension: '2d',
+ baseArrayLayer: z,
+ arrayLayerCount: 1,
+ baseMipLevel: 0,
+ mipLevelCount: 1,
+ }),
+ },
+ ],
+ });
+ renderPass.setBindGroup(0, bindGroup);
+ renderPass.draw(6);
+ renderPass.end();
+ }
+
+ this.queue.submit([encoder.finish()]);
+ }
+
+ DoCopyTextureToBufferWithDepthAspectTest(
+ format: DepthStencilFormat,
+ copySize: readonly [number, number, number],
+ bytesPerRowPadding: number,
+ rowsPerImagePadding: number,
+ offset: number,
+ dataPaddingInBytes: number,
+ mipLevel: number
+ ): void {
+ // [2]: need to convert the float32 values in initialData into the ones compatible
+ // to the depth aspect of depthFormats when depth16unorm is supported by the browsers.
+
+ // Generate the initial depth data uploaded to the texture as float32.
+ const initialData = new Float32Array(copySize[0] * copySize[1] * copySize[2]);
+ for (let i = 0; i < initialData.length; ++i) {
+ const baseValue = 0.05 * i;
+
+ // We expect there are both 1's and 0's in initialData.
+ initialData[i] = i % 40 === 0 ? 1 : baseValue - Math.floor(baseValue);
+ assert(initialData[i] >= 0 && initialData[i] <= 1);
+ }
+
+ // The data uploaded to the texture, using the byte pattern of the format.
+ let formatInitialData: TypedArrayBufferView = initialData;
+
+ // For unorm depth formats, replace the uploaded depth data with quantized data to avoid
+ // rounding issues when converting from 32float to 16unorm.
+ if (format === 'depth16unorm') {
+ const u16Data = new Uint16Array(initialData.length);
+ for (let i = 0; i < initialData.length; i++) {
+ u16Data[i] = initialData[i] * 65535;
+ initialData[i] = u16Data[i] / 65535.0;
+ }
+ formatInitialData = u16Data;
+ }
+
+ // Initialize the depth aspect of the source texture
+ const depthTexture = this.device.createTexture({
+ format,
+ size: [copySize[0] << mipLevel, copySize[1] << mipLevel, copySize[2]] as const,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ mipLevelCount: mipLevel + 1,
+ });
+ this.trackForCleanup(depthTexture);
+ this.initializeDepthAspectWithRendering(depthTexture, format, copySize, mipLevel, initialData);
+
+ // Copy the depth aspect of the texture into the destination buffer.
+ const aspectBytesPerBlock = depthStencilFormatAspectSize(format, 'depth-only');
+ const bytesPerRow =
+ align(aspectBytesPerBlock * copySize[0], kBytesPerRowAlignment) +
+ bytesPerRowPadding * kBytesPerRowAlignment;
+ const rowsPerImage = copySize[1] + rowsPerImagePadding;
+
+ const destinationBufferSize = align(
+ bytesPerRow * rowsPerImage * copySize[2] +
+ bytesPerRow * (copySize[1] - 1) +
+ aspectBytesPerBlock * copySize[0] +
+ offset +
+ dataPaddingInBytes,
+ kBufferSizeAlignment
+ );
+ const destinationBuffer = this.device.createBuffer({
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ size: destinationBufferSize,
+ });
+ this.trackForCleanup(destinationBuffer);
+ const copyEncoder = this.device.createCommandEncoder();
+ copyEncoder.copyTextureToBuffer(
+ {
+ texture: depthTexture,
+ mipLevel,
+ aspect: 'depth-only',
+ },
+ {
+ buffer: destinationBuffer,
+ offset,
+ bytesPerRow,
+ rowsPerImage,
+ },
+ copySize
+ );
+ this.queue.submit([copyEncoder.finish()]);
+
+ // Validate the data in destinationBuffer is what we expect.
+ const expectedData = new Uint8Array(destinationBufferSize);
+ for (let z = 0; z < copySize[2]; ++z) {
+ const baseExpectedOffset = z * bytesPerRow * rowsPerImage + offset;
+ const baseInitialDataOffset = z * copySize[0] * copySize[1];
+ for (let y = 0; y < copySize[1]; ++y) {
+ memcpy(
+ {
+ src: formatInitialData,
+ start: baseInitialDataOffset + y * copySize[0],
+ length: copySize[0],
+ },
+ { dst: expectedData, start: baseExpectedOffset + y * bytesPerRow }
+ );
+ }
+ }
+ this.expectGPUBufferValuesEqual(destinationBuffer, expectedData);
+ }
+}
+
+/**
+ * This is a helper function used for filtering test parameters
+ *
+ * [3]: Modify this after introducing tests with rendering.
+ */
+function formatCanBeTested({ format }: { format: SizedTextureFormat }): boolean {
+ return kTextureFormatInfo[format].copyDst && kTextureFormatInfo[format].copySrc;
+}
+
+export const g = makeTestGroup(ImageCopyTest);
+
+const kRowsPerImageAndBytesPerRowParams = {
+ paddings: [
+ { bytesPerRowPadding: 0, rowsPerImagePadding: 0 }, // no padding
+ { bytesPerRowPadding: 0, rowsPerImagePadding: 6 }, // rowsPerImage padding
+ { bytesPerRowPadding: 6, rowsPerImagePadding: 0 }, // bytesPerRow padding
+ { bytesPerRowPadding: 15, rowsPerImagePadding: 17 }, // both paddings
+ ],
+
+ copySizes: [
+ // In the two cases below, for (WriteTexture, PartialCopyB2T) and (CopyB2T, FullCopyT2B)
+ // sets of methods we will have bytesPerRow = 256 and copyDepth % 2 == { 0, 1 }
+ // respectively. This covers a special code path for D3D12.
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5 }, // standard copy
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 2 }, // standard copy
+
+ { copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5 }, // empty copy because of width
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5 }, // empty copy because of height
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0 }, // empty copy because of depthOrArrayLayers
+ { copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2 }, // copyWidth is 256-aligned
+ { copyWidthInBlocks: 1, copyHeightInBlocks: 3, copyDepth: 5 }, // copyWidth = 1
+
+ // The two cases below cover another special code path for D3D12.
+ // - For (WriteTexture, FullCopyT2B) with r8unorm:
+ // bytesPerRow = 15 = 3 * 5 = bytesInACompleteCopyImage.
+ { copyWidthInBlocks: 32, copyHeightInBlocks: 1, copyDepth: 8 }, // copyHeight = 1
+ // - For (CopyB2T, FullCopyT2B) and (WriteTexture, PartialCopyT2B) with r8unorm:
+ // bytesPerRow = 256 = 8 * 32 = bytesInACompleteCopyImage.
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1 }, // copyDepth = 1
+
+ { copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1 }, // copyHeight = 1 and copyDepth = 1
+ ],
+
+ // Copy sizes that are suitable for 1D texture and check both some copy sizes and empty copies.
+ copySizes1D: [
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 1, copyDepth: 1 },
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 1, copyDepth: 1 },
+
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 1 },
+ { copyWidthInBlocks: 0, copyHeightInBlocks: 1, copyDepth: 1 },
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 1, copyDepth: 0 },
+ ],
+};
+
+g.test('rowsPerImage_and_bytesPerRow')
+ .desc(
+ `Test that copying data with various bytesPerRow and rowsPerImage values and minimum required
+bytes in copy works for every format.
+
+ Covers a special code path for Metal:
+ bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
+ Covers a special code path for D3D12:
+ when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1: copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
+ bytesPerRow == bytesInACompleteCopyImage
+
+ TODO: Cover the special code paths for 3D textures in D3D12.
+ `
+ )
+ .params(u =>
+ u
+ .combineWithParams(kMethodsToTest)
+ .combine('format', kWorkingColorTextureFormats)
+ .filter(formatCanBeTested)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combineWithParams(kRowsPerImageAndBytesPerRowParams.paddings)
+ .expandWithParams(p => {
+ if (p.dimension === '1d') {
+ return kRowsPerImageAndBytesPerRowParams.copySizes1D;
+ }
+ return kRowsPerImageAndBytesPerRowParams.copySizes;
+ })
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ bytesPerRowPadding,
+ rowsPerImagePadding,
+ copyWidthInBlocks,
+ copyHeightInBlocks,
+ copyDepth,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+ // For CopyB2T and CopyT2B we need to have bytesPerRow 256-aligned,
+ // to make this happen we align the bytesInACompleteRow value and multiply
+ // bytesPerRowPadding by 256.
+ const bytesPerRowAlignment =
+ initMethod === 'WriteTexture' && checkMethod === 'FullCopyT2B' ? 1 : 256;
+
+ const copyWidth = copyWidthInBlocks * info.blockWidth;
+ const copyHeight = copyHeightInBlocks * info.blockHeight;
+ const rowsPerImage = copyHeightInBlocks + rowsPerImagePadding;
+ const bytesPerRow =
+ align(bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) +
+ bytesPerRowPadding * bytesPerRowAlignment;
+ const copySize = { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth };
+
+ const dataSize = dataBytesForCopyOrFail({
+ layout: { offset: 0, bytesPerRow, rowsPerImage },
+ format,
+ copySize,
+ method: initMethod,
+ });
+
+ t.uploadTextureAndVerifyCopy({
+ textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
+ copySize,
+ dataSize,
+ textureSize: [
+ Math.max(copyWidth, info.blockWidth),
+ Math.max(copyHeight, info.blockHeight),
+ Math.max(copyDepth, 1),
+ ] /* making sure the texture is non-empty */,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ });
+ });
+
+const kOffsetsAndSizesParams = {
+ offsetsAndPaddings: [
+ { offsetInBlocks: 0, dataPaddingInBytes: 0 }, // no offset and no padding
+ { offsetInBlocks: 1, dataPaddingInBytes: 0 }, // offset = 1
+ { offsetInBlocks: 2, dataPaddingInBytes: 0 }, // offset = 2
+ { offsetInBlocks: 15, dataPaddingInBytes: 0 }, // offset = 15
+ { offsetInBlocks: 16, dataPaddingInBytes: 0 }, // offset = 16
+ { offsetInBlocks: 242, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 242 + 12 = 256 = bytesPerRow
+ { offsetInBlocks: 243, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 243 + 12 > 256 = bytesPerRow
+ { offsetInBlocks: 768, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 768 = 3 * 256 = bytesInACompleteCopyImage
+ { offsetInBlocks: 769, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 769 > 768 = bytesInACompleteCopyImage
+ { offsetInBlocks: 0, dataPaddingInBytes: 1 }, // dataPaddingInBytes > 0
+ { offsetInBlocks: 1, dataPaddingInBytes: 8 }, // offset > 0 and dataPaddingInBytes > 0
+ ],
+ copyDepth: [1, 2],
+};
+
+g.test('offsets_and_sizes')
+ .desc(
+ `Test that copying data with various offset values and additional data paddings
+works for every format with 2d and 2d-array textures.
+
+ Covers two special code paths for D3D12:
+ offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
+ offset > bytesInACompleteCopyImage
+
+ TODO: Cover the special code paths for 3D textures in D3D12.
+ TODO: Make a variant for depth-stencil formats.
+`
+ )
+ .params(u =>
+ u
+ .combineWithParams(kMethodsToTest)
+ .combine('format', kWorkingColorTextureFormats)
+ .filter(formatCanBeTested)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combineWithParams(kOffsetsAndSizesParams.offsetsAndPaddings)
+ .combine('copyDepth', kOffsetsAndSizesParams.copyDepth) // 2d and 2d-array textures
+ .unless(p => p.dimension === '1d' && p.copyDepth !== 1)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ offsetInBlocks,
+ dataPaddingInBytes,
+ copyDepth,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const offset = offsetInBlocks * info.bytesPerBlock;
+ const copySize = {
+ width: 3 * info.blockWidth,
+ height: 3 * info.blockHeight,
+ depthOrArrayLayers: copyDepth,
+ };
+ let textureHeight = 4 * info.blockHeight;
+ let rowsPerImage = 3;
+ const bytesPerRow = 256;
+
+ if (dimension === '1d') {
+ copySize.height = 1;
+ textureHeight = info.blockHeight;
+ rowsPerImage = 1;
+ }
+ const textureSize = [4 * info.blockWidth, textureHeight, copyDepth] as const;
+
+ const minDataSize = dataBytesForCopyOrFail({
+ layout: { offset, bytesPerRow, rowsPerImage },
+ format,
+ copySize,
+ method: initMethod,
+ });
+ const dataSize = minDataSize + dataPaddingInBytes;
+
+ // We're copying a (3 x 3 x copyDepth) (in texel blocks) part of a (4 x 4 x copyDepth)
+ // (in texel blocks) texture with no origin.
+ t.uploadTextureAndVerifyCopy({
+ textureDataLayout: { offset, bytesPerRow, rowsPerImage },
+ copySize,
+ dataSize,
+ textureSize,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ });
+ });
+
+g.test('origins_and_extents')
+ .desc(
+ `Test that copying slices of a texture works with various origin and copyExtent values
+for all formats. We pass origin and copyExtent as [number, number, number].`
+ )
+ .params(u =>
+ u
+ .combineWithParams(kMethodsToTest)
+ .combine('format', kWorkingColorTextureFormats)
+ .filter(formatCanBeTested)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('originValueInBlocks', [0, 7, 8])
+ .combine('copySizeValueInBlocks', [0, 7, 8])
+ .combine('textureSizePaddingValueInBlocks', [0, 7, 8])
+ .unless(
+ p =>
+ // we can't create an empty texture
+ p.copySizeValueInBlocks + p.originValueInBlocks + p.textureSizePaddingValueInBlocks === 0
+ )
+ .combine('coordinateToTest', [0, 1, 2] as const)
+ .unless(p => p.dimension === '1d' && p.coordinateToTest !== 0)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ originValueInBlocks,
+ copySizeValueInBlocks,
+ textureSizePaddingValueInBlocks,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ let originBlocks = [1, 1, 1];
+ let copySizeBlocks = [2, 2, 2];
+ let texSizeBlocks = [3, 3, 3];
+ if (dimension === '1d') {
+ originBlocks = [1, 0, 0];
+ copySizeBlocks = [2, 1, 1];
+ texSizeBlocks = [3, 1, 1];
+ }
+
+ {
+ const ctt = t.params.coordinateToTest;
+ originBlocks[ctt] = originValueInBlocks;
+ copySizeBlocks[ctt] = copySizeValueInBlocks;
+ texSizeBlocks[ctt] =
+ originBlocks[ctt] + copySizeBlocks[ctt] + textureSizePaddingValueInBlocks;
+ }
+
+ const origin: Required<GPUOrigin3DDict> = {
+ x: originBlocks[0] * info.blockWidth,
+ y: originBlocks[1] * info.blockHeight,
+ z: originBlocks[2],
+ };
+ const copySize = {
+ width: copySizeBlocks[0] * info.blockWidth,
+ height: copySizeBlocks[1] * info.blockHeight,
+ depthOrArrayLayers: copySizeBlocks[2],
+ };
+ const textureSize = [
+ texSizeBlocks[0] * info.blockWidth,
+ texSizeBlocks[1] * info.blockHeight,
+ texSizeBlocks[2],
+ ] as const;
+
+ const rowsPerImage = copySizeBlocks[1];
+ const bytesPerRow = align(copySizeBlocks[0] * info.bytesPerBlock, 256);
+
+ const dataSize = dataBytesForCopyOrFail({
+ layout: { offset: 0, bytesPerRow, rowsPerImage },
+ format,
+ copySize,
+ method: initMethod,
+ });
+
+ // For testing width: we copy a (_ x 2 x 2) (in texel blocks) part of a (_ x 3 x 3)
+ // (in texel blocks) texture with origin (_, 1, 1) (in texel blocks).
+ // Similarly for other coordinates.
+ t.uploadTextureAndVerifyCopy({
+ textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
+ copySize,
+ dataSize,
+ origin,
+ textureSize,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ changeBeforePass: 'arrays',
+ });
+ });
+
+/**
+ * Generates textureSizes which correspond to the same physicalSizeAtMipLevel including virtual
+ * sizes at mip level different from the physical ones.
+ */
+function* generateTestTextureSizes({
+ format,
+ dimension,
+ mipLevel,
+ _mipSizeInBlocks,
+}: {
+ format: SizedTextureFormat;
+ dimension: GPUTextureDimension;
+ mipLevel: number;
+ _mipSizeInBlocks: Required<GPUExtent3DDict>;
+}): Generator<[number, number, number]> {
+ assert(dimension !== '1d'); // textureSize[1] would be wrong for 1D mipped textures.
+ const info = kTextureFormatInfo[format];
+
+ const widthAtThisLevel = _mipSizeInBlocks.width * info.blockWidth;
+ const heightAtThisLevel = _mipSizeInBlocks.height * info.blockHeight;
+ const textureSize: [number, number, number] = [
+ widthAtThisLevel << mipLevel,
+ heightAtThisLevel << mipLevel,
+ _mipSizeInBlocks.depthOrArrayLayers << (dimension === '3d' ? mipLevel : 0),
+ ];
+ yield textureSize;
+
+ // We choose width and height of the texture so that the values are divisible by blockWidth and
+ // blockHeight respectively and so that the virtual size at mip level corresponds to the same
+ // physical size.
+ // Virtual size at mip level with modified width has width = (physical size width) - (blockWidth / 2).
+ // Virtual size at mip level with modified height has height = (physical size height) - (blockHeight / 2).
+ const widthAtPrevLevel = widthAtThisLevel << 1;
+ const heightAtPrevLevel = heightAtThisLevel << 1;
+ assert(mipLevel > 0);
+ assert(widthAtPrevLevel >= info.blockWidth && heightAtPrevLevel >= info.blockHeight);
+ const modifiedWidth = (widthAtPrevLevel - info.blockWidth) << (mipLevel - 1);
+ const modifiedHeight = (heightAtPrevLevel - info.blockHeight) << (mipLevel - 1);
+
+ const modifyWidth = info.blockWidth > 1 && modifiedWidth !== textureSize[0];
+ const modifyHeight = info.blockHeight > 1 && modifiedHeight !== textureSize[1];
+
+ if (modifyWidth) {
+ yield [modifiedWidth, textureSize[1], textureSize[2]];
+ }
+ if (modifyHeight) {
+ yield [textureSize[0], modifiedHeight, textureSize[2]];
+ }
+ if (modifyWidth && modifyHeight) {
+ yield [modifiedWidth, modifiedHeight, textureSize[2]];
+ }
+
+ if (dimension === '3d') {
+ yield [textureSize[0], textureSize[1], textureSize[2] + 1];
+ }
+}
+
+g.test('mip_levels')
+ .desc(
+ `Test that copying various mip levels works. Covers two special code paths:
+ - The physical size of the subresource is not equal to the logical size.
+ - bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers, and copyExtent needs to be clamped for all block formats.
+ - For 3D textures test copying to a sub-range of the depth.
+
+Tests both 2D and 3D textures. 1D textures are skipped because they can only have one mip level.
+
+TODO: Make a variant for depth-stencil formats.
+ `
+ )
+ .params(u =>
+ u
+ .combineWithParams(kMethodsToTest)
+ .combine('format', kWorkingColorTextureFormats)
+ .filter(formatCanBeTested)
+ .combine('dimension', ['2d', '3d'] as const)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combineWithParams([
+ // origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d texture */
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 1 },
+ originInBlocks: { x: 3, y: 2, z: 0 },
+ _mipSizeInBlocks: { width: 8, height: 6, depthOrArrayLayers: 1 },
+ mipLevel: 1,
+ },
+ // origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d-array texture
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
+ originInBlocks: { x: 3, y: 2, z: 1 },
+ _mipSizeInBlocks: { width: 8, height: 6, depthOrArrayLayers: 3 },
+ mipLevel: 2,
+ },
+ // origin.x + copySize.width = texturePhysicalSizeAtMipLevel.width
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
+ originInBlocks: { x: 3, y: 2, z: 1 },
+ _mipSizeInBlocks: { width: 8, height: 7, depthOrArrayLayers: 4 },
+ mipLevel: 3,
+ },
+ // origin.y + copySize.height = texturePhysicalSizeAtMipLevel.height
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
+ originInBlocks: { x: 3, y: 2, z: 1 },
+ _mipSizeInBlocks: { width: 9, height: 6, depthOrArrayLayers: 4 },
+ mipLevel: 4,
+ },
+ // origin.z + copySize.depthOrArrayLayers = texturePhysicalSizeAtMipLevel.depthOrArrayLayers
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
+ originInBlocks: { x: 3, y: 2, z: 1 },
+ _mipSizeInBlocks: { width: 9, height: 7, depthOrArrayLayers: 3 },
+ mipLevel: 4,
+ },
+ // origin + copySize < texturePhysicalSizeAtMipLevel for all coordinates
+ {
+ copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
+ originInBlocks: { x: 3, y: 2, z: 1 },
+ _mipSizeInBlocks: { width: 9, height: 7, depthOrArrayLayers: 4 },
+ mipLevel: 4,
+ },
+ ])
+ .expand('textureSize', generateTestTextureSizes)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ copySizeInBlocks,
+ originInBlocks,
+ textureSize,
+ mipLevel,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const origin = {
+ x: originInBlocks.x * info.blockWidth,
+ y: originInBlocks.y * info.blockHeight,
+ z: originInBlocks.z,
+ };
+ const copySize = {
+ width: copySizeInBlocks.width * info.blockWidth,
+ height: copySizeInBlocks.height * info.blockHeight,
+ depthOrArrayLayers: copySizeInBlocks.depthOrArrayLayers,
+ };
+
+ const rowsPerImage = copySizeInBlocks.height + 1;
+ const bytesPerRow = align(copySize.width, 256);
+
+ const dataSize = dataBytesForCopyOrFail({
+ layout: { offset: 0, bytesPerRow, rowsPerImage },
+ format,
+ copySize,
+ method: initMethod,
+ });
+
+ t.uploadTextureAndVerifyCopy({
+ textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
+ copySize,
+ dataSize,
+ origin,
+ mipLevel,
+ textureSize,
+ format,
+ dimension,
+ initMethod,
+ checkMethod,
+ });
+ });
+
+const UND = undefined;
+g.test('undefined_params')
+ .desc(
+ `Tests undefined values of bytesPerRow, rowsPerImage, and origin.x/y/z.
+ Ensures bytesPerRow/rowsPerImage=undefined are valid and behave as expected.
+ Ensures origin.x/y/z undefined default to 0.`
+ )
+ .params(u =>
+ u
+ .combineWithParams(kMethodsToTest)
+ .combine('dimension', kTextureDimensions)
+ .beginSubcases()
+ .combineWithParams([
+ // copying one row: bytesPerRow and rowsPerImage can be undefined
+ { copySize: [3, 1, 1], origin: [UND, UND, UND], bytesPerRow: UND, rowsPerImage: UND },
+ // copying one slice: rowsPerImage can be undefined
+ { copySize: [3, 1, 1], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: UND },
+ { copySize: [3, 3, 1], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: UND },
+ // copying two slices
+ { copySize: [3, 3, 2], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: 3 },
+ // origin.x = undefined
+ { copySize: [1, 1, 1], origin: [UND, 1, 1], bytesPerRow: UND, rowsPerImage: UND },
+ // origin.y = undefined
+ { copySize: [1, 1, 1], origin: [1, UND, 1], bytesPerRow: UND, rowsPerImage: UND },
+ // origin.z = undefined
+ { copySize: [1, 1, 1], origin: [1, 1, UND], bytesPerRow: UND, rowsPerImage: UND },
+ ])
+ .expandWithParams(p => [
+ {
+ _textureSize: [
+ 100,
+ p.copySize[1] + (p.origin[1] ?? 0),
+ p.copySize[2] + (p.origin[2] ?? 0),
+ ] as const,
+ },
+ ])
+ .unless(p => p.dimension === '1d' && (p._textureSize[1] > 1 || p._textureSize[2] > 1))
+ )
+ .fn(async t => {
+ const {
+ dimension,
+ _textureSize,
+ bytesPerRow,
+ rowsPerImage,
+ copySize,
+ origin,
+ initMethod,
+ checkMethod,
+ } = t.params;
+
+ t.uploadTextureAndVerifyCopy({
+ textureDataLayout: {
+ offset: 0,
+ // Zero will get turned back into undefined later.
+ bytesPerRow: bytesPerRow ?? 0,
+ // Zero will get turned back into undefined later.
+ rowsPerImage: rowsPerImage ?? 0,
+ },
+ copySize: { width: copySize[0], height: copySize[1], depthOrArrayLayers: copySize[2] },
+ dataSize: 2000,
+ textureSize: _textureSize,
+ // Zeros will get turned back into undefined later.
+ origin: { x: origin[0] ?? 0, y: origin[1] ?? 0, z: origin[2] ?? 0 },
+ format: 'rgba8unorm',
+ dimension,
+ initMethod,
+ checkMethod,
+ changeBeforePass: 'undefined',
+ });
+ });
+
+function CopyMethodSupportedWithDepthStencilFormat(
+ aspect: 'depth-only' | 'stencil-only',
+ format: DepthStencilFormat,
+ copyMethod: 'WriteTexture' | 'CopyB2T' | 'CopyT2B'
+): boolean {
+ {
+ return (
+ (aspect === 'stencil-only' && kTextureFormatInfo[format].stencil) ||
+ (aspect === 'depth-only' &&
+ kTextureFormatInfo[format].depth &&
+ copyMethod === 'CopyT2B' &&
+ depthStencilBufferTextureCopySupported('CopyT2B', format, aspect))
+ );
+ }
+}
+
+g.test('rowsPerImage_and_bytesPerRow_depth_stencil')
+ .desc(
+ `Test that copying data with various bytesPerRow and rowsPerImage values and minimum required
+bytes in copy works for copyBufferToTexture(), copyTextureToBuffer() and writeTexture() with stencil
+aspect and copyTextureToBuffer() with depth aspect.
+
+ Covers a special code path for Metal:
+ bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
+ Covers a special code path for D3D12:
+ when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1:
+ copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
+ bytesPerRow == bytesInACompleteCopyImage
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .combine('copyMethod', ['WriteTexture', 'CopyB2T', 'CopyT2B'] as const)
+ .combine('aspect', ['depth-only', 'stencil-only'] as const)
+ .filter(t => CopyMethodSupportedWithDepthStencilFormat(t.aspect, t.format, t.copyMethod))
+ .beginSubcases()
+ .combineWithParams(kRowsPerImageAndBytesPerRowParams.paddings)
+ .combineWithParams(kRowsPerImageAndBytesPerRowParams.copySizes)
+ .filter(t => {
+ return t.copyWidthInBlocks * t.copyHeightInBlocks * t.copyDepth > 0;
+ })
+ .combine('mipLevel', [0, 2])
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ format,
+ copyMethod,
+ aspect,
+ bytesPerRowPadding,
+ rowsPerImagePadding,
+ copyWidthInBlocks,
+ copyHeightInBlocks,
+ copyDepth,
+ mipLevel,
+ } = t.params;
+ const bytesPerBlock = depthStencilFormatAspectSize(format, aspect);
+ const rowsPerImage = copyHeightInBlocks + rowsPerImagePadding;
+
+ const bytesPerRowAlignment = copyMethod === 'WriteTexture' ? 1 : kBytesPerRowAlignment;
+ const bytesPerRow =
+ align(bytesPerBlock * copyWidthInBlocks, bytesPerRowAlignment) +
+ bytesPerRowPadding * bytesPerRowAlignment;
+
+ const copySize = [copyWidthInBlocks, copyHeightInBlocks, copyDepth] as const;
+ const textureSize = [
+ copyWidthInBlocks << mipLevel,
+ copyHeightInBlocks << mipLevel,
+ copyDepth,
+ ] as const;
+ if (copyMethod === 'CopyT2B') {
+ if (aspect === 'depth-only') {
+ t.DoCopyTextureToBufferWithDepthAspectTest(
+ format,
+ copySize,
+ bytesPerRowPadding,
+ rowsPerImagePadding,
+ 0,
+ 0,
+ mipLevel
+ );
+ } else {
+ await t.DoCopyFromStencilTest(format, textureSize, bytesPerRow, rowsPerImage, 0, mipLevel);
+ }
+ } else {
+ assert(
+ aspect === 'stencil-only' && (copyMethod === 'CopyB2T' || copyMethod === 'WriteTexture')
+ );
+ const initialDataSize = dataBytesForCopyOrFail({
+ layout: { bytesPerRow, rowsPerImage },
+ format: 'stencil8',
+ copySize,
+ method: copyMethod,
+ });
+
+ await t.DoUploadToStencilTest(
+ format,
+ textureSize,
+ copyMethod,
+ bytesPerRow,
+ rowsPerImage,
+ initialDataSize,
+ 0,
+ mipLevel
+ );
+ }
+ });
+
+g.test('offsets_and_sizes_copy_depth_stencil')
+ .desc(
+ `Test that copying data with various offset values and additional data paddings
+works for copyBufferToTexture(), copyTextureToBuffer() and writeTexture() with stencil aspect and
+copyTextureToBuffer() with depth aspect.
+
+ Covers two special code paths for D3D12:
+ offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
+ offset > bytesInACompleteCopyImage
+`
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .combine('copyMethod', ['WriteTexture', 'CopyB2T', 'CopyT2B'] as const)
+ .combine('aspect', ['depth-only', 'stencil-only'] as const)
+ .filter(t => CopyMethodSupportedWithDepthStencilFormat(t.aspect, t.format, t.copyMethod))
+ .beginSubcases()
+ .combineWithParams(kOffsetsAndSizesParams.offsetsAndPaddings)
+ .filter(t => t.offsetInBlocks % 4 === 0)
+ .combine('copyDepth', kOffsetsAndSizesParams.copyDepth)
+ .combine('mipLevel', [0, 2])
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ format,
+ copyMethod,
+ aspect,
+ offsetInBlocks,
+ dataPaddingInBytes,
+ copyDepth,
+ mipLevel,
+ } = t.params;
+ const bytesPerBlock = depthStencilFormatAspectSize(format, aspect);
+ const initialDataOffset = offsetInBlocks * bytesPerBlock;
+ const copySize = [3, 3, copyDepth] as const;
+ const rowsPerImage = 3;
+ const bytesPerRow = 256;
+
+ const textureSize = [copySize[0] << mipLevel, copySize[1] << mipLevel, copyDepth] as const;
+ if (copyMethod === 'CopyT2B') {
+ if (aspect === 'depth-only') {
+ t.DoCopyTextureToBufferWithDepthAspectTest(format, copySize, 0, 0, 0, 0, mipLevel);
+ } else {
+ await t.DoCopyFromStencilTest(
+ format,
+ textureSize,
+ bytesPerRow,
+ rowsPerImage,
+ initialDataOffset,
+ mipLevel
+ );
+ }
+ } else {
+ assert(
+ aspect === 'stencil-only' && (copyMethod === 'CopyB2T' || copyMethod === 'WriteTexture')
+ );
+ const minDataSize = dataBytesForCopyOrFail({
+ layout: { offset: initialDataOffset, bytesPerRow, rowsPerImage },
+ format: 'stencil8',
+ copySize,
+ method: copyMethod,
+ });
+ const initialDataSize = minDataSize + dataPaddingInBytes;
+ await t.DoUploadToStencilTest(
+ format,
+ textureSize,
+ copyMethod,
+ bytesPerRow,
+ rowsPerImage,
+ initialDataSize,
+ initialDataOffset,
+ mipLevel
+ );
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/programmable_state_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/programmable_state_test.ts
new file mode 100644
index 0000000000..19cf91419c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/programmable_state_test.ts
@@ -0,0 +1,157 @@
+import { unreachable } from '../../../../../common/util/util.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { EncoderType } from '../../../../util/command_buffer_maker.js';
+
+interface BindGroupIndices {
+ a: number;
+ b: number;
+ out: number;
+}
+
+export class ProgrammableStateTest extends GPUTest {
+ private commonBindGroupLayouts: Map<string, GPUBindGroupLayout> = new Map();
+
+ getBindGroupLayout(type: GPUBufferBindingType): GPUBindGroupLayout {
+ if (!this.commonBindGroupLayouts.has(type)) {
+ this.commonBindGroupLayouts.set(
+ type,
+ this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
+ buffer: { type },
+ },
+ ],
+ })
+ );
+ }
+ return this.commonBindGroupLayouts.get(type)!;
+ }
+
+ getBindGroupLayouts(indices: BindGroupIndices): GPUBindGroupLayout[] {
+ const bindGroupLayouts: GPUBindGroupLayout[] = [];
+ bindGroupLayouts[indices.a] = this.getBindGroupLayout('read-only-storage');
+ bindGroupLayouts[indices.b] = this.getBindGroupLayout('read-only-storage');
+ bindGroupLayouts[indices.out] = this.getBindGroupLayout('storage');
+ return bindGroupLayouts;
+ }
+
+ createBindGroup(buffer: GPUBuffer, type: GPUBufferBindingType): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout: this.getBindGroupLayout(type),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ }
+
+ setBindGroup(
+ encoder: GPUBindingCommandsMixin,
+ index: number,
+ factory: (index: number) => GPUBindGroup
+ ) {
+ encoder.setBindGroup(index, factory(index));
+ }
+
+ // Create a compute pipeline that performs an operation on data from two bind groups,
+ // then writes the result to a third bind group.
+ createBindingStatePipeline<T extends EncoderType>(
+ encoderType: T,
+ groups: BindGroupIndices,
+ algorithm: string = 'a.value - b.value'
+ ): GPUComputePipeline | GPURenderPipeline {
+ switch (encoderType) {
+ case 'compute pass': {
+ const wgsl = `struct Data {
+ value : i32
+ };
+
+ @group(${groups.a}) @binding(0) var<storage> a : Data;
+ @group(${groups.b}) @binding(0) var<storage> b : Data;
+ @group(${groups.out}) @binding(0) var<storage, read_write> out : Data;
+
+ @compute @workgroup_size(1) fn main() {
+ out.value = ${algorithm};
+ return;
+ }
+ `;
+
+ return this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: this.getBindGroupLayouts(groups),
+ }),
+ compute: {
+ module: this.device.createShaderModule({
+ code: wgsl,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ }
+ case 'render pass':
+ case 'render bundle': {
+ const wgslShaders = {
+ vertex: `
+ @vertex fn vert_main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.5, 0.5, 0.0, 1.0);
+ }
+ `,
+
+ fragment: `
+ struct Data {
+ value : i32
+ };
+
+ @group(${groups.a}) @binding(0) var<storage> a : Data;
+ @group(${groups.b}) @binding(0) var<storage> b : Data;
+ @group(${groups.out}) @binding(0) var<storage, read_write> out : Data;
+
+ @fragment fn frag_main() -> @location(0) vec4<f32> {
+ out.value = ${algorithm};
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ `,
+ };
+
+ return this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: this.getBindGroupLayouts(groups),
+ }),
+ vertex: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.vertex,
+ }),
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.fragment,
+ }),
+ entryPoint: 'frag_main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ }
+ default:
+ unreachable();
+ }
+ }
+
+ setPipeline(pass: GPUBindingCommandsMixin, pipeline: GPUComputePipeline | GPURenderPipeline) {
+ if (pass instanceof GPUComputePassEncoder) {
+ pass.setPipeline(pipeline as GPUComputePipeline);
+ } else if (pass instanceof GPURenderPassEncoder || pass instanceof GPURenderBundleEncoder) {
+ pass.setPipeline(pipeline as GPURenderPipeline);
+ }
+ }
+
+ dispatchOrDraw(pass: GPUBindingCommandsMixin) {
+ if (pass instanceof GPUComputePassEncoder) {
+ pass.dispatchWorkgroups(1);
+ } else if (pass instanceof GPURenderPassEncoder) {
+ pass.draw(1);
+ } else if (pass instanceof GPURenderBundleEncoder) {
+ pass.draw(1);
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/state_tracking.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/state_tracking.spec.ts
new file mode 100644
index 0000000000..a27793244e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/programmable/state_tracking.spec.ts
@@ -0,0 +1,306 @@
+export const description = `
+Ensure state is set correctly. Tries to stress state caching (setting different states multiple
+times in different orders) for setBindGroup and setPipeline.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../constants.js';
+import { kProgrammableEncoderTypes } from '../../../../util/command_buffer_maker.js';
+
+import { ProgrammableStateTest } from './programmable_state_test.js';
+
+export const g = makeTestGroup(ProgrammableStateTest);
+
+const kBufferUsage = GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.STORAGE;
+
+g.test('bind_group_indices')
+ .desc(
+ `
+ Test that bind group indices can be declared in any order, regardless of their order in the shader.
+ - Test places the value of buffer a - buffer b into the out buffer, then reads the result.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .beginSubcases()
+ .combine('groupIndices', [
+ { a: 0, b: 1, out: 2 },
+ { a: 1, b: 2, out: 0 },
+ { a: 2, b: 0, out: 1 },
+ { a: 0, b: 2, out: 1 },
+ { a: 2, b: 1, out: 0 },
+ { a: 1, b: 0, out: 2 },
+ ])
+ )
+ .fn(async t => {
+ const { encoderType, groupIndices } = t.params;
+
+ const pipeline = t.createBindingStatePipeline(encoderType, groupIndices);
+
+ const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const bindGroups = {
+ a: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ b: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ out: t.createBindGroup(out, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+
+ t.setPipeline(encoder, pipeline);
+ encoder.setBindGroup(groupIndices.a, bindGroups.a);
+ encoder.setBindGroup(groupIndices.b, bindGroups.b);
+ encoder.setBindGroup(groupIndices.out, bindGroups.out);
+ t.dispatchOrDraw(encoder);
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
+ });
+
+g.test('bind_group_order')
+ .desc(
+ `
+ Test that the order in which you set the bind groups doesn't matter.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .beginSubcases()
+ .combine('setOrder', [
+ ['a', 'b', 'out'],
+ ['b', 'out', 'a'],
+ ['out', 'a', 'b'],
+ ['b', 'a', 'out'],
+ ['a', 'out', 'b'],
+ ['out', 'b', 'a'],
+ ] as const)
+ )
+ .fn(async t => {
+ const { encoderType, setOrder } = t.params;
+
+ const groupIndices = { a: 0, b: 1, out: 2 };
+ const pipeline = t.createBindingStatePipeline(encoderType, groupIndices);
+
+ const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const bindGroups = {
+ a: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ b: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ out: t.createBindGroup(out, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+ t.setPipeline(encoder, pipeline);
+
+ for (const bindingName of setOrder) {
+ encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
+ }
+
+ t.dispatchOrDraw(encoder);
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
+ });
+
+g.test('bind_group_before_pipeline')
+ .desc(
+ `
+ Test that setting bind groups prior to setting the pipeline is still valid.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .beginSubcases()
+ .combineWithParams([
+ { setBefore: ['a', 'b'], setAfter: ['out'] },
+ { setBefore: ['a'], setAfter: ['b', 'out'] },
+ { setBefore: ['out', 'b'], setAfter: ['a'] },
+ { setBefore: ['a', 'b', 'out'], setAfter: [] },
+ ] as const)
+ )
+ .fn(async t => {
+ const { encoderType, setBefore, setAfter } = t.params;
+ const groupIndices = { a: 0, b: 1, out: 2 };
+ const pipeline = t.createBindingStatePipeline(encoderType, groupIndices);
+
+ const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const bindGroups = {
+ a: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ b: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ out: t.createBindGroup(out, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+
+ for (const bindingName of setBefore) {
+ encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
+ }
+
+ t.setPipeline(encoder, pipeline);
+
+ for (const bindingName of setAfter) {
+ encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
+ }
+
+ t.dispatchOrDraw(encoder);
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
+ });
+
+g.test('one_bind_group_multiple_slots')
+ .desc(
+ `
+ Test that a single bind group may be bound to more than one slot.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ )
+ .fn(async t => {
+ const { encoderType } = t.params;
+ const pipeline = t.createBindingStatePipeline(encoderType, { a: 0, b: 1, out: 2 });
+
+ const out = t.makeBufferWithContents(new Int32Array([1]), kBufferUsage);
+ const bindGroups = {
+ ab: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ out: t.createBindGroup(out, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+ t.setPipeline(encoder, pipeline);
+
+ encoder.setBindGroup(0, bindGroups.ab);
+ encoder.setBindGroup(1, bindGroups.ab);
+ encoder.setBindGroup(2, bindGroups.out);
+
+ t.dispatchOrDraw(encoder);
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(out, new Int32Array([0]));
+ });
+
+g.test('bind_group_multiple_sets')
+ .desc(
+ `
+ Test that the last bind group set to a given slot is used when dispatching.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ )
+ .fn(async t => {
+ const { encoderType } = t.params;
+ const pipeline = t.createBindingStatePipeline(encoderType, { a: 0, b: 1, out: 2 });
+
+ const badOut = t.makeBufferWithContents(new Int32Array([-1]), kBufferUsage);
+ const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const bindGroups = {
+ a: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ b: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ c: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([5]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ badOut: t.createBindGroup(badOut, 'storage'),
+ out: t.createBindGroup(out, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+
+ encoder.setBindGroup(1, bindGroups.c);
+
+ t.setPipeline(encoder, pipeline);
+
+ encoder.setBindGroup(0, bindGroups.c);
+ encoder.setBindGroup(0, bindGroups.a);
+
+ encoder.setBindGroup(2, bindGroups.badOut);
+
+ encoder.setBindGroup(1, bindGroups.b);
+ encoder.setBindGroup(2, bindGroups.out);
+
+ t.dispatchOrDraw(encoder);
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
+ t.expectGPUBufferValuesEqual(badOut, new Int32Array([-1]));
+ });
+
+g.test('compatible_pipelines')
+ .desc('Test that bind groups can be shared between compatible pipelines.')
+ .params(u =>
+ u //
+ .combine('encoderType', kProgrammableEncoderTypes)
+ )
+ .fn(async t => {
+ const { encoderType } = t.params;
+ const pipelineA = t.createBindingStatePipeline(encoderType, { a: 0, b: 1, out: 2 });
+ const pipelineB = t.createBindingStatePipeline(
+ encoderType,
+ { a: 0, b: 1, out: 2 },
+ 'a.value + b.value'
+ );
+
+ const outA = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const outB = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
+ const bindGroups = {
+ a: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ b: t.createBindGroup(
+ t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
+ 'read-only-storage'
+ ),
+ outA: t.createBindGroup(outA, 'storage'),
+ outB: t.createBindGroup(outB, 'storage'),
+ };
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+ encoder.setBindGroup(0, bindGroups.a);
+ encoder.setBindGroup(1, bindGroups.b);
+
+ t.setPipeline(encoder, pipelineA);
+ encoder.setBindGroup(2, bindGroups.outA);
+ t.dispatchOrDraw(encoder);
+
+ t.setPipeline(encoder, pipelineB);
+ encoder.setBindGroup(2, bindGroups.outB);
+ t.dispatchOrDraw(encoder);
+
+ validateFinishAndSubmit(true, true);
+
+ t.expectGPUBufferValuesEqual(outA, new Int32Array([1]));
+ t.expectGPUBufferValuesEqual(outB, new Int32Array([5]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/README.txt
new file mode 100644
index 0000000000..0118a6c537
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/queries/README.txt
@@ -0,0 +1,8 @@
+TODO: test the behavior of creating/using/resolving queries.
+- occlusion
+- pipeline statistics
+ TODO: pipeline statistics queries are removed from core; consider moving tests to another suite.
+- timestamp
+- nested (e.g. timestamp or PS query inside occlusion query), if any such cases are valid. Try
+ writing to the same query set (at same or different indices), if valid. Check results make sense.
+- start a query (all types) with no draw calls
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/dynamic_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/dynamic_state.spec.ts
new file mode 100644
index 0000000000..d342fb6a46
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/dynamic_state.spec.ts
@@ -0,0 +1,19 @@
+export const description = `
+Tests of the behavior of the viewport/scissor/blend/reference states.
+
+TODO:
+- {viewport, scissor rect, blend color, stencil reference}:
+ Test rendering result with {various values}.
+ - Set the state in different ways to make sure it gets the correct value in the end: {
+ - state unset (= default)
+ - state explicitly set once to {default value, another value}
+ - persistence: [set, draw, draw] (fn should differentiate from [set, draw] + [draw])
+ - overwriting: [set(1), draw, set(2), draw] (fn should differentiate from [set(1), set(2), draw, draw])
+ - overwriting: [set(1), set(2), draw] (fn should differentiate from [set(1), draw] but not [set(2), draw])
+ - }
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/state_tracking.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/state_tracking.spec.ts
new file mode 100644
index 0000000000..fc904066a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/render/state_tracking.spec.ts
@@ -0,0 +1,631 @@
+export const description = `
+Ensure state is set correctly. Tries to stress state caching (setting different states multiple
+times in different orders) for setIndexBuffer and setVertexBuffer.
+Equivalent tests for setBindGroup and setPipeline are in programmable/state_tracking.spec.ts.
+Equivalent tests for viewport/scissor/blend/reference are in render/dynamic_state.spec.ts
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+
+class VertexAndIndexStateTrackingTest extends GPUTest {
+ GetRenderPipelineForTest(arrayStride: number): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Inputs {
+ @location(0) vertexPosition : f32,
+ @location(1) vertexColor : vec4<f32>,
+ };
+ struct Outputs {
+ @builtin(position) position : vec4<f32>,
+ @location(0) color : vec4<f32>,
+ };
+ @vertex
+ fn main(input : Inputs)-> Outputs {
+ var outputs : Outputs;
+ outputs.position =
+ vec4<f32>(input.vertexPosition, 0.5, 0.0, 1.0);
+ outputs.color = input.vertexColor;
+ return outputs;
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride,
+ attributes: [
+ {
+ format: 'float32',
+ offset: 0,
+ shaderLocation: 0,
+ },
+ {
+ format: 'unorm8x4',
+ offset: 4,
+ shaderLocation: 1,
+ },
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Input {
+ @location(0) color : vec4<f32>
+ };
+ @fragment
+ fn main(input : Input) -> @location(0) vec4<f32> {
+ return input.color;
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+ }
+
+ kVertexAttributeSize = 8;
+}
+
+export const g = makeTestGroup(VertexAndIndexStateTrackingTest);
+
+g.test('set_index_buffer_without_changing_buffer')
+ .desc(
+ `
+ Test that setting index buffer states (index format, offset, size) multiple times in different
+ orders still keeps the correctness of each draw call.
+`
+ )
+ .fn(async t => {
+ // Initialize the index buffer with 5 uint16 indices (0, 1, 2, 3, 4).
+ const indexBuffer = t.makeBufferWithContents(
+ new Uint16Array([0, 1, 2, 3, 4]),
+ GPUBufferUsage.INDEX
+ );
+
+ // Initialize the vertex buffer with required vertex attributes (position: f32, color: f32x4)
+ // Note that the maximum index in the test is 0x10000.
+ const kVertexAttributesCount = 0x10000 + 1;
+ const vertexBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.VERTEX,
+ size: t.kVertexAttributeSize * kVertexAttributesCount,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(vertexBuffer);
+ const vertexAttributes = vertexBuffer.getMappedRange();
+ const kPositions = [-0.8, -0.4, 0.0, 0.4, 0.8, -0.4];
+ const kColors = [
+ new Uint8Array([255, 0, 0, 255]),
+ new Uint8Array([255, 255, 255, 255]),
+ new Uint8Array([0, 0, 255, 255]),
+ new Uint8Array([255, 0, 255, 255]),
+ new Uint8Array([0, 255, 255, 255]),
+ new Uint8Array([0, 255, 0, 255]),
+ ];
+ // Set vertex attributes at index {0..4} in Uint16.
+ // Note that the vertex attribute at index 1 will not be used.
+ for (let i = 0; i < kPositions.length - 1; ++i) {
+ const baseOffset = t.kVertexAttributeSize * i;
+ const vertexPosition = new Float32Array(vertexAttributes, baseOffset, 1);
+ vertexPosition[0] = kPositions[i];
+ const vertexColor = new Uint8Array(vertexAttributes, baseOffset + 4, 4);
+ vertexColor.set(kColors[i]);
+ }
+ // Set vertex attributes at index 0x10000.
+ const lastOffset = t.kVertexAttributeSize * (kVertexAttributesCount - 1);
+ const lastVertexPosition = new Float32Array(vertexAttributes, lastOffset, 1);
+ lastVertexPosition[0] = kPositions[kPositions.length - 1];
+ const lastVertexColor = new Uint8Array(vertexAttributes, lastOffset + 4, 4);
+ lastVertexColor.set(kColors[kColors.length - 1]);
+
+ vertexBuffer.unmap();
+
+ const renderPipeline = t.GetRenderPipelineForTest(t.kVertexAttributeSize);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kPositions.length - 1, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(renderPipeline);
+ renderPass.setVertexBuffer(0, vertexBuffer);
+
+ // 1st draw: indexFormat = 'uint32', offset = 0, size = 4 (index value: 0x10000)
+ renderPass.setIndexBuffer(indexBuffer, 'uint32', 0, 4);
+ renderPass.drawIndexed(1);
+
+ // 2nd draw: indexFormat = 'uint16', offset = 0, size = 4 (index value: 0)
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', 0, 4);
+ renderPass.drawIndexed(1);
+
+ // 3rd draw: indexFormat = 'uint16', offset = 4, size = 2 (index value: 2)
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', 0, 2);
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', 4, 2);
+ renderPass.drawIndexed(1);
+
+ // 4th draw: indexformat = 'uint16', offset = 6, size = 4 (index values: 3, 4)
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', 6, 2);
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', 6, 4);
+ renderPass.drawIndexed(2);
+
+ renderPass.end();
+ t.queue.submit([encoder.finish()]);
+
+ for (let i = 0; i < kPositions.length - 1; ++i) {
+ const expectedColor = i === 1 ? kColors[kPositions.length - 1] : kColors[i];
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ 'rgba8unorm',
+ { x: i, y: 0 },
+ { exp: expectedColor }
+ );
+ }
+ });
+
+g.test('set_vertex_buffer_without_changing_buffer')
+ .desc(
+ `
+ Test that setting vertex buffer states (offset, size) multiple times in different orders still
+ keeps the correctness of each draw call.
+ - Tries several different sequences of setVertexBuffer+draw commands, each of which draws vertices
+ in all 4 output pixels, and check they were drawn correctly.
+`
+ )
+ .fn(async t => {
+ const kPositions = [-0.875, -0.625, -0.375, -0.125, 0.125, 0.375, 0.625, 0.875];
+ const kColors = [
+ new Uint8Array([255, 0, 0, 255]),
+ new Uint8Array([0, 255, 0, 255]),
+ new Uint8Array([0, 0, 255, 255]),
+ new Uint8Array([51, 0, 0, 255]),
+ new Uint8Array([0, 51, 0, 255]),
+ new Uint8Array([0, 0, 51, 255]),
+ new Uint8Array([255, 0, 255, 255]),
+ new Uint8Array([255, 255, 0, 255]),
+ ];
+
+ // Initialize the vertex buffer with required vertex attributes (position: f32, color: f32x4)
+ const kVertexAttributesCount = 8;
+ const vertexBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.VERTEX,
+ size: t.kVertexAttributeSize * kVertexAttributesCount,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(vertexBuffer);
+ const vertexAttributes = vertexBuffer.getMappedRange();
+ for (let i = 0; i < kPositions.length; ++i) {
+ const baseOffset = t.kVertexAttributeSize * i;
+ const vertexPosition = new Float32Array(vertexAttributes, baseOffset, 1);
+ vertexPosition[0] = kPositions[i];
+ const vertexColor = new Uint8Array(vertexAttributes, baseOffset + 4, 4);
+ vertexColor.set(kColors[i]);
+ }
+
+ vertexBuffer.unmap();
+
+ const renderPipeline = t.GetRenderPipelineForTest(t.kVertexAttributeSize);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kPositions.length, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(renderPipeline);
+
+ // Change 'size' in setVertexBuffer()
+ renderPass.setVertexBuffer(0, vertexBuffer, 0, t.kVertexAttributeSize);
+ renderPass.setVertexBuffer(0, vertexBuffer, 0, t.kVertexAttributeSize * 2);
+ renderPass.draw(2);
+
+ // Change 'offset' in setVertexBuffer()
+ renderPass.setVertexBuffer(
+ 0,
+ vertexBuffer,
+ t.kVertexAttributeSize * 2,
+ t.kVertexAttributeSize * 2
+ );
+ renderPass.draw(2);
+
+ // Change 'size' again in setVertexBuffer()
+ renderPass.setVertexBuffer(
+ 0,
+ vertexBuffer,
+ t.kVertexAttributeSize * 4,
+ t.kVertexAttributeSize * 2
+ );
+ renderPass.setVertexBuffer(
+ 0,
+ vertexBuffer,
+ t.kVertexAttributeSize * 4,
+ t.kVertexAttributeSize * 4
+ );
+ renderPass.draw(4);
+
+ renderPass.end();
+ t.queue.submit([encoder.finish()]);
+
+ for (let i = 0; i < kPositions.length; ++i) {
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ 'rgba8unorm',
+ { x: i, y: 0 },
+ { exp: kColors[i] }
+ );
+ }
+ });
+
+g.test('change_pipeline_before_and_after_vertex_buffer')
+ .desc(
+ `
+ Test that changing the pipeline {before,after} the vertex buffers still keeps the correctness of
+ each draw call (In D3D12, the vertex buffer stride is part of SetVertexBuffer instead of the
+ pipeline.)
+`
+ )
+ .fn(async t => {
+ const kPositions = [-0.8, -0.4, 0.0, 0.4, 0.8, 0.9];
+ const kColors = [
+ new Uint8Array([255, 0, 0, 255]),
+ new Uint8Array([255, 255, 255, 255]),
+ new Uint8Array([0, 255, 0, 255]),
+ new Uint8Array([0, 0, 255, 255]),
+ new Uint8Array([255, 0, 255, 255]),
+ new Uint8Array([0, 255, 255, 255]),
+ ];
+
+ // Initialize the vertex buffer with required vertex attributes (position: f32, color: f32x4)
+ const vertexBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.VERTEX,
+ size: t.kVertexAttributeSize * kPositions.length,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(vertexBuffer);
+ // Note that kPositions[1], kColors[1], kPositions[5] and kColors[5] are not used.
+ const vertexAttributes = vertexBuffer.getMappedRange();
+ for (let i = 0; i < kPositions.length; ++i) {
+ const baseOffset = t.kVertexAttributeSize * i;
+ const vertexPosition = new Float32Array(vertexAttributes, baseOffset, 1);
+ vertexPosition[0] = kPositions[i];
+ const vertexColor = new Uint8Array(vertexAttributes, baseOffset + 4, 4);
+ vertexColor.set(kColors[i]);
+ }
+ vertexBuffer.unmap();
+
+ // Create two render pipelines with different vertex attribute strides
+ const renderPipeline1 = t.GetRenderPipelineForTest(t.kVertexAttributeSize);
+ const renderPipeline2 = t.GetRenderPipelineForTest(t.kVertexAttributeSize * 2);
+
+ const kPointsCount = kPositions.length - 1;
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kPointsCount, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ // Update render pipeline before setVertexBuffer. The applied vertex attribute stride should be
+ // 2 * kVertexAttributeSize.
+ renderPass.setPipeline(renderPipeline1);
+ renderPass.setPipeline(renderPipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer);
+ renderPass.draw(2);
+
+ // Update render pipeline after setVertexBuffer. The applied vertex attribute stride should be
+ // kVertexAttributeSize.
+ renderPass.setVertexBuffer(0, vertexBuffer, 3 * t.kVertexAttributeSize);
+ renderPass.setPipeline(renderPipeline1);
+ renderPass.draw(2);
+
+ renderPass.end();
+
+ t.queue.submit([encoder.finish()]);
+
+ for (let i = 0; i < kPointsCount; ++i) {
+ const expectedColor = i === 1 ? new Uint8Array([0, 0, 0, 255]) : kColors[i];
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ 'rgba8unorm',
+ { x: i, y: 0 },
+ { exp: expectedColor }
+ );
+ }
+ });
+
+g.test('set_vertex_buffer_but_not_used_in_draw')
+ .desc(
+ `
+ Test that drawing after having set vertex buffer slots not used by the pipeline works correctly.
+ - In the test there are 2 draw calls in the render pass. The first draw call uses 2 vertex buffers
+ (position and color), and the second draw call only uses 1 vertex buffer (for color, the vertex
+ position is defined as constant values in the vertex shader). The test verifies if both of these
+ two draw calls work correctly.
+ `
+ )
+ .fn(async t => {
+ const kPositions = new Float32Array([-0.75, -0.25]);
+ const kColors = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
+
+ // Initialize the vertex buffers with required vertex attributes (position: f32, color: f32x4)
+ const kAttributeStride = 4;
+ const positionBuffer = t.makeBufferWithContents(kPositions, GPUBufferUsage.VERTEX);
+ const colorBuffer = t.makeBufferWithContents(kColors, GPUBufferUsage.VERTEX);
+
+ const fragmentState: GPUFragmentState = {
+ module: t.device.createShaderModule({
+ code: `
+ struct Input {
+ @location(0) color : vec4<f32>
+ };
+ @fragment
+ fn main(input : Input) -> @location(0) vec4<f32> {
+ return input.color;
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ };
+
+ // Create renderPipeline1 that uses both positionBuffer and colorBuffer.
+ const renderPipeline1 = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Inputs {
+ @location(0) vertexColor : vec4<f32>,
+ @location(1) vertexPosition : f32,
+ };
+ struct Outputs {
+ @builtin(position) position : vec4<f32>,
+ @location(0) color : vec4<f32>,
+ };
+ @vertex
+ fn main(input : Inputs)-> Outputs {
+ var outputs : Outputs;
+ outputs.position =
+ vec4<f32>(input.vertexPosition, 0.5, 0.0, 1.0);
+ outputs.color = input.vertexColor;
+ return outputs;
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride: kAttributeStride,
+ attributes: [
+ {
+ format: 'unorm8x4',
+ offset: 0,
+ shaderLocation: 0,
+ },
+ ],
+ },
+ {
+ arrayStride: kAttributeStride,
+ attributes: [
+ {
+ format: 'float32',
+ offset: 0,
+ shaderLocation: 1,
+ },
+ ],
+ },
+ ],
+ },
+ fragment: fragmentState,
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const renderPipeline2 = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Inputs {
+ @builtin(vertex_index) vertexIndex : u32,
+ @location(0) vertexColor : vec4<f32>,
+ };
+ struct Outputs {
+ @builtin(position) position : vec4<f32>,
+ @location(0) color : vec4<f32>,
+ };
+ @vertex
+ fn main(input : Inputs)-> Outputs {
+ var kPositions = array<f32, 2> (0.25, 0.75);
+ var outputs : Outputs;
+ outputs.position =
+ vec4(kPositions[input.vertexIndex], 0.5, 0.0, 1.0);
+ outputs.color = input.vertexColor;
+ return outputs;
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride: kAttributeStride,
+ attributes: [
+ {
+ format: 'unorm8x4',
+ offset: 0,
+ shaderLocation: 0,
+ },
+ ],
+ },
+ ],
+ },
+ fragment: fragmentState,
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const kPointsCount = 4;
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kPointsCount, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ renderPass.setVertexBuffer(0, colorBuffer);
+ renderPass.setVertexBuffer(1, positionBuffer);
+ renderPass.setPipeline(renderPipeline1);
+ renderPass.draw(2);
+
+ renderPass.setPipeline(renderPipeline2);
+ renderPass.draw(2);
+
+ renderPass.end();
+
+ t.queue.submit([encoder.finish()]);
+
+ const kExpectedColors = [
+ kColors.subarray(0, 4),
+ kColors.subarray(4),
+ kColors.subarray(0, 4),
+ kColors.subarray(4),
+ ];
+
+ for (let i = 0; i < kPointsCount; ++i) {
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ 'rgba8unorm',
+ { x: i, y: 0 },
+ { exp: kExpectedColors[i] }
+ );
+ }
+ });
+
+g.test('set_index_buffer_before_non_indexed_draw')
+ .desc(
+ `
+ Test that setting / not setting the index buffer does not impact a non-indexed draw.
+ `
+ )
+ .fn(async t => {
+ const kPositions = [-0.75, -0.25, 0.25, 0.75];
+ const kColors = [
+ new Uint8Array([255, 0, 0, 255]),
+ new Uint8Array([0, 255, 0, 255]),
+ new Uint8Array([0, 0, 255, 255]),
+ new Uint8Array([255, 0, 255, 255]),
+ ];
+
+ // Initialize the vertex buffer with required vertex attributes (position: f32, color: f32x4)
+ const vertexBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.VERTEX,
+ size: t.kVertexAttributeSize * kPositions.length,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(vertexBuffer);
+ const vertexAttributes = vertexBuffer.getMappedRange();
+ for (let i = 0; i < kPositions.length; ++i) {
+ const baseOffset = t.kVertexAttributeSize * i;
+ const vertexPosition = new Float32Array(vertexAttributes, baseOffset, 1);
+ vertexPosition[0] = kPositions[i];
+ const vertexColor = new Uint8Array(vertexAttributes, baseOffset + 4, 4);
+ vertexColor.set(kColors[i]);
+ }
+ vertexBuffer.unmap();
+
+ // Initialize the index buffer with 2 uint16 indices (2, 3).
+ const indexBuffer = t.makeBufferWithContents(new Uint16Array([2, 3]), GPUBufferUsage.INDEX);
+
+ const renderPipeline = t.GetRenderPipelineForTest(t.kVertexAttributeSize);
+
+ const kPointsCount = 4;
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kPointsCount, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ // The first draw call is an indexed one (the third and fourth color are involved)
+ renderPass.setVertexBuffer(0, vertexBuffer);
+ renderPass.setIndexBuffer(indexBuffer, 'uint16');
+ renderPass.setPipeline(renderPipeline);
+ renderPass.drawIndexed(2);
+
+ // The second draw call is a non-indexed one (the first and second color are involved)
+ renderPass.draw(2);
+
+ renderPass.end();
+
+ t.queue.submit([encoder.finish()]);
+
+ for (let i = 0; i < kPointsCount; ++i) {
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ 'rgba8unorm',
+ { x: i, y: 0 },
+ { exp: kColors[i] }
+ );
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute/basic.spec.ts
new file mode 100644
index 0000000000..a00d72b37c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute/basic.spec.ts
@@ -0,0 +1,163 @@
+export const description = `
+Basic command buffer compute tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kLimitInfo } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { checkElementsEqualGenerated } from '../../../util/check_contents.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const kMaxComputeWorkgroupSize = [
+ kLimitInfo.maxComputeWorkgroupSizeX.default,
+ kLimitInfo.maxComputeWorkgroupSizeY.default,
+ kLimitInfo.maxComputeWorkgroupSizeZ.default,
+];
+
+g.test('memcpy').fn(async t => {
+ const data = new Uint32Array([0x01020304]);
+
+ const src = t.makeBufferWithContents(data, GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE);
+
+ const dst = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Data {
+ value : u32
+ };
+
+ @group(0) @binding(0) var<storage, read> src : Data;
+ @group(0) @binding(1) var<storage, read_write> dst : Data;
+
+ @compute @workgroup_size(1) fn main() {
+ dst.value = src.value;
+ return;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [
+ { binding: 0, resource: { buffer: src, offset: 0, size: 4 } },
+ { binding: 1, resource: { buffer: dst, offset: 0, size: 4 } },
+ ],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(dst, data);
+});
+
+g.test('large_dispatch')
+ .desc(`Test reasonably-sized large dispatches (see also: stress tests).`)
+ .params(u =>
+ u
+ // Reasonably-sized powers of two, and some stranger larger sizes.
+ .combine('dispatchSize', [
+ 256,
+ 2048,
+ 315,
+ 628,
+ 2179,
+ kLimitInfo.maxComputeWorkgroupsPerDimension.default,
+ ])
+ // Test some reasonable workgroup sizes.
+ .beginSubcases()
+ // 0 == x axis; 1 == y axis; 2 == z axis.
+ .combine('largeDimension', [0, 1, 2] as const)
+ .expand('workgroupSize', p => [1, 2, 8, 32, kMaxComputeWorkgroupSize[p.largeDimension]])
+ )
+ .fn(async t => {
+ // The output storage buffer is filled with this value.
+ const val = 0x01020304;
+ const badVal = 0xbaadf00d;
+
+ const wgSize = t.params.workgroupSize;
+ const bufferLength = t.params.dispatchSize * wgSize;
+ const bufferByteSize = Uint32Array.BYTES_PER_ELEMENT * bufferLength;
+ const dst = t.device.createBuffer({
+ size: bufferByteSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+
+ // Only use one large dimension and workgroup size in the dispatch
+ // call to keep the size of the test reasonable.
+ const dims = [1, 1, 1];
+ dims[t.params.largeDimension] = t.params.dispatchSize;
+ const wgSizes = [1, 1, 1];
+ wgSizes[t.params.largeDimension] = t.params.workgroupSize;
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ struct OutputBuffer {
+ value : array<u32>
+ };
+
+ @group(0) @binding(0) var<storage, read_write> dst : OutputBuffer;
+
+ @compute @workgroup_size(${wgSizes[0]}, ${wgSizes[1]}, ${wgSizes[2]})
+ fn main(
+ @builtin(global_invocation_id) GlobalInvocationID : vec3<u32>
+ ) {
+ var xExtent : u32 = ${dims[0]}u * ${wgSizes[0]}u;
+ var yExtent : u32 = ${dims[1]}u * ${wgSizes[1]}u;
+ var zExtent : u32 = ${dims[2]}u * ${wgSizes[2]}u;
+ var index : u32 = (
+ GlobalInvocationID.z * xExtent * yExtent +
+ GlobalInvocationID.y * xExtent +
+ GlobalInvocationID.x);
+ var val : u32 = ${val}u;
+ // Trivial error checking in the indexing and invocation.
+ if (GlobalInvocationID.x > xExtent ||
+ GlobalInvocationID.y > yExtent ||
+ GlobalInvocationID.z > zExtent) {
+ val = ${badVal}u;
+ }
+ dst.value[index] = val;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer: dst, offset: 0, size: bufferByteSize } }],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(dims[0], dims[1], dims[2]);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesPassCheck(dst, a => checkElementsEqualGenerated(a, i => val), {
+ type: Uint32Array,
+ typedLength: bufferLength,
+ });
+
+ dst.destroy();
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/entry_point_name.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/entry_point_name.spec.ts
new file mode 100644
index 0000000000..a62031c3fd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/entry_point_name.spec.ts
@@ -0,0 +1,12 @@
+export const description = `
+TODO:
+- Test some weird but valid values for entry point name (both module and pipeline creation
+ should succeed).
+- Test using each of many entry points in the module (should succeed).
+- Test using an entry point with the wrong stage (should fail).
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/overrides.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/overrides.spec.ts
new file mode 100644
index 0000000000..09248a0b41
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/compute_pipeline/overrides.spec.ts
@@ -0,0 +1,503 @@
+export const description = `
+Compute pipeline using overridable constants test.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+class F extends GPUTest {
+ async ExpectShaderOutputWithConstants(
+ isAsync: boolean,
+ expected: Uint32Array | Float32Array,
+ constants: Record<string, GPUPipelineConstantValue>,
+ code: string
+ ) {
+ const dst = this.device.createBuffer({
+ size: expected.byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+
+ const descriptor: GPUComputePipelineDescriptor = {
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({
+ code,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ const promise = isAsync
+ ? this.device.createComputePipelineAsync(descriptor)
+ : Promise.resolve(this.device.createComputePipeline(descriptor));
+
+ const pipeline = await promise;
+ const bindGroup = this.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer: dst, offset: 0, size: expected.byteLength } }],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ this.expectGPUBufferValuesEqual(dst, expected);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('basic')
+ .desc(
+ `Test that either correct constants override values or default values when no constants override value are provided at pipeline creation time are used as the output to the storage buffer.`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const count = 11;
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ new Uint32Array(range(count, i => i)),
+ {
+ c0: 0,
+ c1: 1,
+ c2: 2,
+ c3: 3,
+ // c4 is using default value
+ c5: 5,
+ c6: 6,
+ // c7 is using default value
+ c8: 8,
+ c9: 9,
+ // c10 is using default value
+ },
+ `
+ override c0: bool; // type: bool
+ override c1: bool = false; // default override
+ override c2: f32; // type: float32
+ override c3: f32 = 0.0; // default override
+ override c4: f32 = 4.0; // default
+ override c5: i32; // type: int32
+ override c6: i32 = 0; // default override
+ override c7: i32 = 7; // default
+ override c8: u32; // type: uint32
+ override c9: u32 = 0u; // default override
+ override c10: u32 = 10u; // default
+
+ struct Buf {
+ data : array<u32, ${count}>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(1) fn main() {
+ buf.data[0] = u32(c0);
+ buf.data[1] = u32(c1);
+ buf.data[2] = u32(c2);
+ buf.data[3] = u32(c3);
+ buf.data[4] = u32(c4);
+ buf.data[5] = u32(c5);
+ buf.data[6] = u32(c6);
+ buf.data[7] = u32(c7);
+ buf.data[8] = u32(c8);
+ buf.data[9] = u32(c9);
+ buf.data[10] = u32(c10);
+ }
+ `
+ );
+ });
+
+g.test('numeric_id')
+ .desc(
+ `Test that correct values are used as output to the storage buffer for constants specified with numeric id instead of their names.`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ new Uint32Array([1, 2, 3]),
+ {
+ 1001: 1,
+ 1: 2,
+ // 1003 is using default value
+ },
+ `
+ @id(1001) override c1: u32; // some big numeric id
+ @id(1) override c2: u32 = 0u; // id == 1 might collide with some generated constant id
+ @id(1003) override c3: u32 = 3u; // default
+
+ struct Buf {
+ data : array<u32, 3>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(1) fn main() {
+ buf.data[0] = c1;
+ buf.data[1] = c2;
+ buf.data[2] = c3;
+ }
+ `
+ );
+ });
+
+g.test('precision')
+ .desc(
+ `Test that float number precision is preserved for constants as they are used for compute shader output of the storage buffer.`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const c1 = 3.14159;
+ const c2 = 3.141592653589793238;
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ // These values will get rounded to f32 and createComputePipeline, so the values coming out from the shader won't be the exact same one as shown here.
+ new Float32Array([c1, c2]),
+ {
+ c1,
+ c2,
+ },
+ `
+ override c1: f32;
+ override c2: f32;
+
+ struct Buf {
+ data : array<f32, 2>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(1) fn main() {
+ buf.data[0] = c1;
+ buf.data[1] = c2;
+ }
+ `
+ );
+ });
+
+g.test('workgroup_size')
+ .desc(
+ `Test that constants can be used as workgroup size correctly, the compute shader should write the max local invocation id to the storage buffer which is equal to the workgroup size dimension given by the constant.`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('type', ['u32', 'i32'])
+ .combine('size', [3, 16, 64])
+ .combine('v', ['x', 'y', 'z'])
+ )
+ .fn(async t => {
+ const { isAsync, type, size, v } = t.params;
+ const workgroup_size_str = v === 'x' ? 'd' : v === 'y' ? '1, d' : '1, 1, d';
+ await t.ExpectShaderOutputWithConstants(
+ isAsync,
+ new Uint32Array([size]),
+ {
+ d: size,
+ },
+ `
+ override d: ${type};
+
+ struct Buf {
+ data : array<u32, 1>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(${workgroup_size_str}) fn main(
+ @builtin(local_invocation_id) local_invocation_id : vec3<u32>
+ ) {
+ if (local_invocation_id.${v} >= u32(d - 1)) {
+ buf.data[0] = local_invocation_id.${v} + 1;
+ }
+ }
+ `
+ );
+ });
+
+g.test('shared_shader_module')
+ .desc(
+ `Test that when the same shader module is shared by different pipelines, the correct constant values are used as output to the storage buffer. The constant value should not affect other pipeline sharing the same shader module.`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ override a: u32;
+
+ struct Buf {
+ data : array<u32, 1>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(1) fn main() {
+ buf.data[0] = a;
+ }`,
+ });
+
+ const expects = [new Uint32Array([1]), new Uint32Array([2])];
+ const buffers = [
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ ];
+
+ const descriptors: GPUComputePipelineDescriptor[] = [
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main',
+ constants: {
+ a: 1,
+ },
+ },
+ },
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main',
+ constants: {
+ a: 2,
+ },
+ },
+ },
+ ];
+
+ const promises = t.params.isAsync
+ ? Promise.all([
+ t.device.createComputePipelineAsync(descriptors[0]),
+ t.device.createComputePipelineAsync(descriptors[1]),
+ ])
+ : Promise.resolve([
+ t.device.createComputePipeline(descriptors[0]),
+ t.device.createComputePipeline(descriptors[1]),
+ ]);
+
+ const pipelines = await promises;
+ const bindGroups = [
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[0], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[0].getBindGroupLayout(0),
+ }),
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[1], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[1].getBindGroupLayout(0),
+ }),
+ ];
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipelines[0]);
+ pass.setBindGroup(0, bindGroups[0]);
+ pass.dispatchWorkgroups(1);
+ pass.setPipeline(pipelines[1]);
+ pass.setBindGroup(0, bindGroups[1]);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(buffers[0], expects[0]);
+ t.expectGPUBufferValuesEqual(buffers[1], expects[1]);
+ });
+
+g.test('multi_entry_points')
+ .desc(
+ `Test that constants used for different entry points are used correctly as output to the storage buffer. They should have no impact for pipeline using entry points that doesn't reference them.`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+ override c1: u32;
+ override c2: u32;
+ override c3: u32;
+
+ struct Buf {
+ data : array<u32, 1>
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buf : Buf;
+
+ @compute @workgroup_size(1) fn main1() {
+ buf.data[0] = c1;
+ }
+
+ @compute @workgroup_size(1) fn main2() {
+ buf.data[0] = c2;
+ }
+
+ @compute @workgroup_size(c3) fn main3() {
+ buf.data[0] = 3u;
+ }`,
+ });
+
+ const expects = [
+ new Uint32Array([1]),
+ new Uint32Array([2]),
+ new Uint32Array([3]),
+ new Uint32Array([4]),
+ ];
+
+ const buffers = [
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ t.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ }),
+ ];
+
+ const descriptors: GPUComputePipelineDescriptor[] = [
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main1',
+ constants: {
+ c1: 1,
+ },
+ },
+ },
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main2',
+ constants: {
+ c2: 2,
+ },
+ },
+ },
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main3',
+ constants: {
+ // c3 is used as workgroup size
+ c3: 1,
+ },
+ },
+ },
+ {
+ layout: 'auto',
+ compute: {
+ module,
+ entryPoint: 'main1',
+ constants: {
+ // assign a different value to c1
+ c1: 4,
+ },
+ },
+ },
+ ];
+
+ const promises = t.params.isAsync
+ ? Promise.all([
+ t.device.createComputePipelineAsync(descriptors[0]),
+ t.device.createComputePipelineAsync(descriptors[1]),
+ t.device.createComputePipelineAsync(descriptors[2]),
+ t.device.createComputePipelineAsync(descriptors[3]),
+ ])
+ : Promise.resolve([
+ t.device.createComputePipeline(descriptors[0]),
+ t.device.createComputePipeline(descriptors[1]),
+ t.device.createComputePipeline(descriptors[2]),
+ t.device.createComputePipeline(descriptors[3]),
+ ]);
+
+ const pipelines = await promises;
+ const bindGroups = [
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[0], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[0].getBindGroupLayout(0),
+ }),
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[1], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[1].getBindGroupLayout(0),
+ }),
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[2], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[2].getBindGroupLayout(0),
+ }),
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: buffers[3], offset: 0, size: Uint32Array.BYTES_PER_ELEMENT },
+ },
+ ],
+ layout: pipelines[3].getBindGroupLayout(0),
+ }),
+ ];
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipelines[0]);
+ pass.setBindGroup(0, bindGroups[0]);
+ pass.dispatchWorkgroups(1);
+ pass.setPipeline(pipelines[1]);
+ pass.setBindGroup(0, bindGroups[1]);
+ pass.dispatchWorkgroups(1);
+ pass.setPipeline(pipelines[2]);
+ pass.setBindGroup(0, bindGroups[2]);
+ pass.dispatchWorkgroups(1);
+ pass.setPipeline(pipelines[3]);
+ pass.setBindGroup(0, bindGroups[3]);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(buffers[0], expects[0]);
+ t.expectGPUBufferValuesEqual(buffers[1], expects[1]);
+ t.expectGPUBufferValuesEqual(buffers[2], expects[2]);
+ t.expectGPUBufferValuesEqual(buffers[3], expects[3]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/device/lost.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/device/lost.spec.ts
new file mode 100644
index 0000000000..88d08b77f5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/device/lost.spec.ts
@@ -0,0 +1,92 @@
+export const description = `
+Tests for GPUDevice.lost.
+`;
+
+import { Fixture } from '../../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { attemptGarbageCollection } from '../../../../common/util/collect_garbage.js';
+import { getGPU } from '../../../../common/util/navigator_gpu.js';
+import {
+ assert,
+ assertNotSettledWithinTime,
+ raceWithRejectOnTimeout,
+} from '../../../../common/util/util.js';
+
+class DeviceLostTests extends Fixture {
+ // Default timeout for waiting for device lost is 2 seconds.
+ readonly kDeviceLostTimeoutMS = 2000;
+
+ getDeviceLostWithTimeout(lost: Promise<GPUDeviceLostInfo>): Promise<GPUDeviceLostInfo> {
+ return raceWithRejectOnTimeout(lost, this.kDeviceLostTimeoutMS, 'device was not lost');
+ }
+
+ expectDeviceDestroyed(device: GPUDevice): void {
+ this.eventualAsyncExpectation(async niceStack => {
+ try {
+ const lost = await this.getDeviceLostWithTimeout(device.lost);
+ this.expect(lost.reason === 'destroyed', 'device was lost from destroy');
+ } catch (ex) {
+ niceStack.message = 'device was not lost';
+ this.rec.expectationFailed(niceStack);
+ }
+ });
+ }
+}
+
+export const g = makeTestGroup(DeviceLostTests);
+
+g.test('not_lost_on_gc')
+ .desc(
+ `'lost' is never resolved by GPUDevice being garbage collected (with attemptGarbageCollection).`
+ )
+ .fn(async t => {
+ // Wraps a lost promise object creation in a function scope so that the device has the best
+ // chance of being gone and ready for GC before trying to resolve the lost promise.
+ const { lost } = await (async () => {
+ const adapter = await getGPU().requestAdapter();
+ assert(adapter !== null);
+ const lost = (await adapter.requestDevice()).lost;
+ return { lost };
+ })();
+ await assertNotSettledWithinTime(lost, t.kDeviceLostTimeoutMS, 'device was unexpectedly lost');
+
+ await attemptGarbageCollection();
+ });
+
+g.test('lost_on_destroy')
+ .desc(`'lost' is resolved, with reason='destroyed', on GPUDevice.destroy().`)
+ .fn(async t => {
+ const adapter = await getGPU().requestAdapter();
+ assert(adapter !== null);
+ const device: GPUDevice = await adapter.requestDevice();
+ t.expectDeviceDestroyed(device);
+ device.destroy();
+ });
+
+g.test('same_object')
+ .desc(`'lost' provides the same Promise and GPUDeviceLostInfo objects each time it's accessed.`)
+ .fn(async t => {
+ const adapter = await getGPU().requestAdapter();
+ assert(adapter !== null);
+ const device: GPUDevice = await adapter.requestDevice();
+
+ // The promises should be the same promise object.
+ const lostPromise1 = device.lost;
+ const lostPromise2 = device.lost;
+ t.expect(lostPromise1 === lostPromise2);
+
+ // Promise object should still be the same after destroy.
+ device.destroy();
+ const lostPromise3 = device.lost;
+ t.expect(lostPromise1 === lostPromise3);
+
+ // The results should also be the same result object.
+ const lost1 = await t.getDeviceLostWithTimeout(lostPromise1);
+ const lost2 = await t.getDeviceLostWithTimeout(lostPromise2);
+ const lost3 = await t.getDeviceLostWithTimeout(lostPromise3);
+ // Promise object should still be the same after we've been notified about device loss.
+ const lostPromise4 = device.lost;
+ t.expect(lostPromise1 === lostPromise4);
+ const lost4 = await t.getDeviceLostWithTimeout(lostPromise4);
+ t.expect(lost1 === lost2 && lost2 === lost3 && lost3 === lost4);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/labels.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/labels.spec.ts
new file mode 100644
index 0000000000..045e40711c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/labels.spec.ts
@@ -0,0 +1,12 @@
+export const description = `
+Tests for object labels.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { GPUTest } from '../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('object_has_descriptor_label')
+ .desc(`For every create function, the descriptor.label is carried over to the object.label.`)
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_allocation/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_allocation/README.txt
new file mode 100644
index 0000000000..a8a8eb1d68
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_allocation/README.txt
@@ -0,0 +1,7 @@
+Try to stress memory allocators in the implementation and driver.
+
+TODO: plan and implement
+- Tests which (pseudo-randomly?) allocate a bunch of memory and then assert things about the memory
+ (it's not aliased, it's valid to read and write in various ways, accesses read/write the correct data)
+ - Possibly also with OOB accesses/robust buffer access?
+- Tests which are targeted against particular known implementation details
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/buffer_sync_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/buffer_sync_test.ts
new file mode 100644
index 0000000000..e18dc59abf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/buffer_sync_test.ts
@@ -0,0 +1,938 @@
+import { assert, unreachable } from '../../../../../common/util/util.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { checkElementsEqualEither } from '../../../../util/check_contents.js';
+import { OperationContext, OperationContextHelper } from '../operation_context_helper.js';
+
+export const kAllWriteOps = ['storage', 'b2b-copy', 't2b-copy', 'write-buffer'] as const;
+
+export const kAllReadOps = [
+ 'input-vertex',
+ 'input-index',
+ 'input-indirect',
+ 'input-indirect-index',
+ 'input-indirect-dispatch',
+
+ 'constant-uniform',
+
+ 'storage-read',
+
+ 'b2b-copy',
+ 'b2t-copy',
+] as const;
+
+export type ReadOp = typeof kAllReadOps[number];
+export type WriteOp = typeof kAllWriteOps[number];
+
+export type Op = ReadOp | WriteOp;
+
+interface OpInfo {
+ readonly contexts: OperationContext[];
+}
+
+const kOpInfo: {
+ readonly [k in Op]: OpInfo;
+} = /* prettier-ignore */ {
+ 'write-buffer': {
+ contexts: [ 'queue' ],
+ },
+ 'b2t-copy': {
+ contexts: [ 'command-encoder' ],
+ },
+ 'b2b-copy': {
+ contexts: [ 'command-encoder' ],
+ },
+ 't2b-copy': {
+ contexts: [ 'command-encoder' ],
+ },
+ 'storage': {
+ contexts: [ 'compute-pass-encoder', 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'storage-read': {
+ contexts: [ 'compute-pass-encoder', 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'input-vertex': {
+ contexts: [ 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'input-index': {
+ contexts: [ 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'input-indirect': {
+ contexts: [ 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'input-indirect-index': {
+ contexts: [ 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+ 'input-indirect-dispatch': {
+ contexts: [ 'compute-pass-encoder' ],
+ },
+ 'constant-uniform': {
+ contexts: [ 'render-pass-encoder', 'render-bundle-encoder' ],
+ },
+};
+
+export function checkOpsValidForContext(
+ ops: [Op, Op],
+ context: [OperationContext, OperationContext]
+) {
+ const valid =
+ kOpInfo[ops[0]].contexts.includes(context[0]) && kOpInfo[ops[1]].contexts.includes(context[1]);
+ if (!valid) return false;
+
+ if (
+ context[0] === 'render-bundle-encoder' ||
+ context[0] === 'render-pass-encoder' ||
+ context[1] === 'render-bundle-encoder' ||
+ context[1] === 'render-pass-encoder'
+ ) {
+ // In a render pass, it is invalid to use a resource as both writable and another usage.
+ // Also, for storage+storage usage, the application is opting into racy behavior.
+ // The storage+storage case is also skipped as the results cannot be reliably tested.
+ const checkImpl = (op1: Op, op2: Op) => {
+ switch (op1) {
+ case 'storage':
+ switch (op2) {
+ case 'storage':
+ case 'storage-read':
+ case 'input-vertex':
+ case 'input-index':
+ case 'input-indirect':
+ case 'input-indirect-index':
+ case 'constant-uniform':
+ // Write+other, or racy.
+ return false;
+ case 'b2t-copy':
+ case 't2b-copy':
+ case 'b2b-copy':
+ case 'write-buffer':
+ // These don't occur in a render pass.
+ return true;
+ }
+ break;
+ case 'input-vertex':
+ case 'input-index':
+ case 'input-indirect':
+ case 'input-indirect-index':
+ case 'constant-uniform':
+ case 'b2t-copy':
+ case 't2b-copy':
+ case 'b2b-copy':
+ case 'write-buffer':
+ // These are not write usages, or don't occur in a render pass.
+ break;
+ }
+ return true;
+ };
+ return checkImpl(ops[0], ops[1]) && checkImpl(ops[1], ops[0]);
+ }
+ return true;
+}
+
+const kDummyVertexShader = `
+@vertex fn vert_main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.5, 0.5, 0.0, 1.0);
+}
+`;
+
+// Note: If it would be useful to have any of these helpers be separate from the fixture,
+// they can be refactored into standalone functions.
+export class BufferSyncTest extends GPUTest {
+ // Vertex and index buffers used in read render pass
+ vertexBuffer?: GPUBuffer;
+ indexBuffer?: GPUBuffer;
+
+ // Temp buffer and texture with values for buffer/texture copy write op
+ // There can be at most 2 write op
+ tmpValueBuffers: (GPUBuffer | undefined)[] = [undefined, undefined];
+ tmpValueTextures: (GPUTexture | undefined)[] = [undefined, undefined];
+
+ // These intermediate buffers/textures are created before any read/write op
+ // to avoid extra memory synchronization between ops introduced by await on buffer/texture creations.
+ // Create extra buffers/textures needed by write operation
+ async createIntermediateBuffersAndTexturesForWriteOp(
+ writeOp: WriteOp,
+ slot: number,
+ value: number
+ ) {
+ switch (writeOp) {
+ case 'b2b-copy':
+ this.tmpValueBuffers[slot] = await this.createBufferWithValue(value);
+ break;
+ case 't2b-copy':
+ this.tmpValueTextures[slot] = await this.createTextureWithValue(value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Create extra buffers/textures needed by read operation
+ async createBuffersForReadOp(readOp: ReadOp, srcValue: number, opValue: number) {
+ // This helps create values that will be written into dst buffer by the readop
+ switch (readOp) {
+ case 'input-index':
+ // The index buffer will be the src buffer of the read op.
+ // The src value for readOp will be 0
+ // If the index buffer value is 0, the src value is written into the dst buffer.
+ // If the index buffer value is 1, the op value is written into the dst buffer.
+ this.vertexBuffer = await this.createBufferWithValues([srcValue, opValue]);
+ break;
+ case 'input-indirect':
+ // The indirect buffer for the draw cmd will be the src buffer of the read op.
+ // If the first value in the indirect buffer is 1, then the op value in vertex buffer will be written into dst buffer.
+ // If the first value in indirect buffer is 0, then nothing will be write into dst buffer.
+ this.vertexBuffer = await this.createBufferWithValues([opValue]);
+ break;
+ case 'input-indirect-index':
+ // The indirect buffer for draw indexed cmd will be the src buffer of the read op.
+ // If the first value in the indirect buffer is 1, then the opValue in vertex buffer will be written into dst buffer.
+ // If the first value in indirect buffer is 0, then nothing will be write into dst buffer.
+ this.vertexBuffer = await this.createBufferWithValues([opValue]);
+ this.indexBuffer = await this.createBufferWithValues([0]);
+ break;
+ default:
+ break;
+ }
+
+ let srcBuffer: GPUBuffer;
+ switch (readOp) {
+ case 'input-indirect':
+ // vertexCount = {0, 1}
+ // instanceCount = 1
+ // firstVertex = 0
+ // firstInstance = 0
+ srcBuffer = await this.createBufferWithValues([srcValue, 1, 0, 0]);
+ break;
+ case 'input-indirect-index':
+ // indexCount = {0, 1}
+ // instanceCount = 1
+ // firstIndex = 0
+ // baseVertex = 0
+ // firstInstance = 0
+ srcBuffer = await this.createBufferWithValues([srcValue, 1, 0, 0, 0]);
+ break;
+ case 'input-indirect-dispatch':
+ // workgroupCountX = {0, 1}
+ // workgroupCountY = 1
+ // workgroupCountZ = 1
+ srcBuffer = await this.createBufferWithValues([srcValue, 1, 1]);
+ break;
+ default:
+ srcBuffer = await this.createBufferWithValue(srcValue);
+ break;
+ }
+
+ const dstBuffer = this.trackForCleanup(
+ this.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage:
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX |
+ GPUBufferUsage.INDIRECT |
+ GPUBufferUsage.UNIFORM,
+ })
+ );
+
+ return { srcBuffer, dstBuffer };
+ }
+
+ // Create a buffer with 1 uint32 element, and initialize it to a specified value.
+ async createBufferWithValue(initValue: number): Promise<GPUBuffer> {
+ const buffer = this.trackForCleanup(
+ this.device.createBuffer({
+ mappedAtCreation: true,
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage:
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX |
+ GPUBufferUsage.INDIRECT |
+ GPUBufferUsage.UNIFORM,
+ })
+ );
+ new Uint32Array(buffer.getMappedRange()).fill(initValue);
+ buffer.unmap();
+ await this.queue.onSubmittedWorkDone();
+ return buffer;
+ }
+
+ // Create a buffer, and initialize it to the specified values.
+ async createBufferWithValues(initValues: number[]): Promise<GPUBuffer> {
+ const buffer = this.trackForCleanup(
+ this.device.createBuffer({
+ mappedAtCreation: true,
+ size: Uint32Array.BYTES_PER_ELEMENT * initValues.length,
+ usage:
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX |
+ GPUBufferUsage.INDIRECT |
+ GPUBufferUsage.UNIFORM,
+ })
+ );
+ const bufferView = new Uint32Array(buffer.getMappedRange());
+ bufferView.set(initValues);
+ buffer.unmap();
+ await this.queue.onSubmittedWorkDone();
+ return buffer;
+ }
+
+ // Create a 1x1 texture, and initialize it to a specified value for all elements.
+ async createTextureWithValue(initValue: number): Promise<GPUTexture> {
+ const data = new Uint32Array(1).fill(initValue);
+ const texture = this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'r32uint',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+ this.device.queue.writeTexture(
+ { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ data,
+ { offset: 0, bytesPerRow: 256, rowsPerImage: 1 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ await this.queue.onSubmittedWorkDone();
+ return texture;
+ }
+
+ createBindGroup(
+ pipeline: GPURenderPipeline | GPUComputePipeline,
+ buffer: GPUBuffer
+ ): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+ }
+
+ // Create a compute pipeline and write given data into storage buffer.
+ createStorageWriteComputePipeline(value: number): GPUComputePipeline {
+ const wgslCompute = `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<storage, read_write> data : Data;
+ @compute @workgroup_size(1) fn main() {
+ data.a = ${value}u;
+ }
+ `;
+
+ return this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({
+ code: wgslCompute,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ }
+
+ createTrivialRenderPipeline(wgslShaders: { vertex: string; fragment: string }) {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.vertex,
+ }),
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.fragment,
+ }),
+ entryPoint: 'frag_main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ }
+
+ // Create a render pipeline and write given data into storage buffer at fragment stage.
+ createStorageWriteRenderPipeline(value: number): GPURenderPipeline {
+ const wgslShaders = {
+ vertex: kDummyVertexShader,
+ fragment: `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<storage, read_write> data : Data;
+ @fragment fn frag_main() -> @location(0) vec4<f32> {
+ data.a = ${value}u;
+ return vec4<f32>(); // result does't matter
+ }
+ `,
+ };
+
+ return this.createTrivialRenderPipeline(wgslShaders);
+ }
+
+ beginSimpleRenderPass(encoder: GPUCommandEncoder): GPURenderPassEncoder {
+ const view = this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ ).createView();
+ return encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+
+ // Write buffer via draw call in render pass. Use bundle if needed.
+ encodeWriteAsStorageBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ buffer: GPUBuffer,
+ value: number
+ ) {
+ const pipeline = this.createStorageWriteRenderPipeline(value);
+ const bindGroup = this.createBindGroup(pipeline, buffer);
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.draw(1, 1, 0, 0);
+ }
+
+ // Write buffer via dispatch call in compute pass.
+ encodeWriteAsStorageBufferInComputePass(
+ pass: GPUComputePassEncoder,
+ buffer: GPUBuffer,
+ value: number
+ ) {
+ const pipeline = this.createStorageWriteComputePipeline(value);
+ const bindGroup = this.createBindGroup(pipeline, buffer);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ }
+
+ // Write buffer via BufferToBuffer copy.
+ encodeWriteByB2BCopy(encoder: GPUCommandEncoder, buffer: GPUBuffer, slot: number) {
+ const tmpBuffer = this.tmpValueBuffers[slot];
+ assert(tmpBuffer !== undefined);
+ // The write operation via b2b copy is just encoded into command encoder, it doesn't write immediately.
+ encoder.copyBufferToBuffer(tmpBuffer, 0, buffer, 0, Uint32Array.BYTES_PER_ELEMENT);
+ }
+
+ // Write buffer via TextureToBuffer copy.
+ encodeWriteByT2BCopy(encoder: GPUCommandEncoder, buffer: GPUBuffer, slot: number) {
+ const tmpTexture = this.tmpValueTextures[slot];
+ assert(tmpTexture !== undefined);
+ // The write operation via t2b copy is just encoded into command encoder, it doesn't write immediately.
+ encoder.copyTextureToBuffer(
+ { texture: tmpTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ }
+
+ // Write buffer via writeBuffer API on queue
+ writeByWriteBuffer(buffer: GPUBuffer, value: number) {
+ const data = new Uint32Array(1).fill(value);
+ this.device.queue.writeBuffer(buffer, 0, data);
+ }
+
+ // Issue write operation via render pass, compute pass, copy, etc.
+ encodeWriteOp(
+ helper: OperationContextHelper,
+ operation: WriteOp,
+ context: OperationContext,
+ buffer: GPUBuffer,
+ writeOpSlot: number,
+ value: number
+ ) {
+ helper.ensureContext(context);
+
+ switch (operation) {
+ case 'write-buffer':
+ this.writeByWriteBuffer(buffer, value);
+ break;
+ case 'storage':
+ switch (context) {
+ case 'render-pass-encoder':
+ assert(helper.renderPassEncoder !== undefined);
+ this.encodeWriteAsStorageBufferInRenderPass(helper.renderPassEncoder, buffer, value);
+ break;
+ case 'render-bundle-encoder':
+ assert(helper.renderBundleEncoder !== undefined);
+ this.encodeWriteAsStorageBufferInRenderPass(helper.renderBundleEncoder, buffer, value);
+ break;
+ case 'compute-pass-encoder':
+ assert(helper.computePassEncoder !== undefined);
+ this.encodeWriteAsStorageBufferInComputePass(helper.computePassEncoder, buffer, value);
+ break;
+ default:
+ unreachable();
+ }
+ break;
+ case 'b2b-copy':
+ assert(helper.commandEncoder !== undefined);
+ this.encodeWriteByB2BCopy(helper.commandEncoder, buffer, writeOpSlot);
+ break;
+ case 't2b-copy':
+ assert(helper.commandEncoder !== undefined);
+ this.encodeWriteByT2BCopy(helper.commandEncoder, buffer, writeOpSlot);
+ break;
+ default:
+ unreachable();
+ }
+ }
+
+ // Create a compute pipeline: read from src buffer and write it into the storage buffer.
+ createStorageReadComputePipeline(): GPUComputePipeline {
+ const wgslCompute = `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<storage, read> srcData : Data;
+ @group(0) @binding(1) var<storage, read_write> dstData : Data;
+
+ @compute @workgroup_size(1) fn main() {
+ dstData.a = srcData.a;
+ }
+ `;
+
+ return this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({
+ code: wgslCompute,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ }
+
+ createBindGroupSrcDstBuffer(
+ pipeline: GPURenderPipeline | GPUComputePipeline,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: { buffer: srcBuffer } },
+ { binding: 1, resource: { buffer: dstBuffer } },
+ ],
+ });
+ }
+
+ // Create a render pipeline: read from vertex/index buffer and write it into the storage dst buffer at fragment stage.
+ createVertexReadRenderPipeline(): GPURenderPipeline {
+ const wgslShaders = {
+ vertex: `
+ struct VertexOutput {
+ @builtin(position) position : vec4<f32>,
+ @location(0) @interpolate(flat) data : u32,
+ };
+
+ @vertex fn vert_main(@location(0) input: u32) -> VertexOutput {
+ var output : VertexOutput;
+ output.position = vec4<f32>(0.5, 0.5, 0.0, 1.0);
+ output.data = input;
+ return output;
+ }
+ `,
+ fragment: `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<storage, read_write> data : Data;
+
+ @fragment fn frag_main(@location(0) @interpolate(flat) input : u32) -> @location(0) vec4<f32> {
+ data.a = input;
+ return vec4<f32>(); // result does't matter
+ }
+ `,
+ };
+
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.vertex,
+ }),
+ entryPoint: 'vert_main',
+ buffers: [
+ {
+ arrayStride: Uint32Array.BYTES_PER_ELEMENT,
+ attributes: [
+ {
+ shaderLocation: 0,
+ offset: 0,
+ format: 'uint32',
+ },
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.fragment,
+ }),
+ entryPoint: 'frag_main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ }
+
+ // Create a render pipeline: read from uniform buffer and write it into the storage dst buffer at fragment stage.
+ createUniformReadRenderPipeline(): GPURenderPipeline {
+ const wgslShaders = {
+ vertex: kDummyVertexShader,
+ fragment: `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<uniform> constant: Data;
+ @group(0) @binding(1) var<storage, read_write> data : Data;
+
+ @fragment fn frag_main() -> @location(0) vec4<f32> {
+ data.a = constant.a;
+ return vec4<f32>(); // result does't matter
+ }
+ `,
+ };
+
+ return this.createTrivialRenderPipeline(wgslShaders);
+ }
+
+ // Create a render pipeline: read from storage src buffer and write it into the storage dst buffer at fragment stage.
+ createStorageReadRenderPipeline(): GPURenderPipeline {
+ const wgslShaders = {
+ vertex: kDummyVertexShader,
+ fragment: `
+ struct Data {
+ a : u32
+ };
+
+ @group(0) @binding(0) var<storage, read> srcData : Data;
+ @group(0) @binding(1) var<storage, read_write> dstData : Data;
+
+ @fragment fn frag_main() -> @location(0) vec4<f32> {
+ dstData.a = srcData.a;
+ return vec4<f32>(); // result does't matter
+ }
+ `,
+ };
+
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.vertex,
+ }),
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: wgslShaders.fragment,
+ }),
+ entryPoint: 'frag_main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ }
+
+ // Write buffer via dispatch call in compute pass.
+ encodeReadAsStorageBufferInComputePass(
+ pass: GPUComputePassEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createStorageReadComputePipeline();
+ const bindGroup = this.createBindGroupSrcDstBuffer(pipeline, srcBuffer, dstBuffer);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ }
+
+ // Write buffer via dispatchWorkgroupsIndirect call in compute pass.
+ encodeReadAsIndirectBufferInComputePass(
+ pass: GPUComputePassEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer,
+ value: number
+ ) {
+ const pipeline = this.createStorageWriteComputePipeline(value);
+ const bindGroup = this.createBindGroup(pipeline, dstBuffer);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroupsIndirect(srcBuffer, 0);
+ }
+
+ // Read as vertex input and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsVertexBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createVertexReadRenderPipeline();
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: dstBuffer } }],
+ });
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.setVertexBuffer(0, srcBuffer);
+ renderer.draw(1);
+ }
+
+ // Read as index input and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsIndexBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer,
+ vertexBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createVertexReadRenderPipeline();
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: dstBuffer } }],
+ });
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.setVertexBuffer(0, vertexBuffer);
+ renderer.setIndexBuffer(srcBuffer, 'uint32');
+ renderer.drawIndexed(1);
+ }
+
+ // Read as indirect input and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsIndirectBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer,
+ vertexBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createVertexReadRenderPipeline();
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: dstBuffer } }],
+ });
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.setVertexBuffer(0, vertexBuffer);
+ renderer.drawIndirect(srcBuffer, 0);
+ }
+
+ // Read as indexed indirect input and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsIndexedIndirectBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer,
+ vertexBuffer: GPUBuffer,
+ indexBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createVertexReadRenderPipeline();
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: dstBuffer } }],
+ });
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.setVertexBuffer(0, vertexBuffer);
+ renderer.setIndexBuffer(indexBuffer, 'uint32');
+ renderer.drawIndexedIndirect(srcBuffer, 0);
+ }
+
+ // Read as uniform buffer and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsUniformBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createUniformReadRenderPipeline();
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: { buffer: srcBuffer } },
+ { binding: 1, resource: { buffer: dstBuffer } },
+ ],
+ });
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.draw(1);
+ }
+
+ // Read as storage buffer and write buffer via draw call in render pass. Use bundle if needed.
+ encodeReadAsStorageBufferInRenderPass(
+ renderer: GPURenderPassEncoder | GPURenderBundleEncoder,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ) {
+ const pipeline = this.createStorageReadRenderPipeline();
+ const bindGroup = this.createBindGroupSrcDstBuffer(pipeline, srcBuffer, dstBuffer);
+
+ renderer.setBindGroup(0, bindGroup);
+ renderer.setPipeline(pipeline);
+ renderer.draw(1, 1, 0, 0);
+ }
+
+ // Read and write via BufferToBuffer copy.
+ encodeReadByB2BCopy(encoder: GPUCommandEncoder, srcBuffer: GPUBuffer, dstBuffer: GPUBuffer) {
+ // The b2b copy is just encoded into command encoder, it doesn't write immediately.
+ encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, Uint32Array.BYTES_PER_ELEMENT);
+ }
+
+ // Read and Write texture via BufferToTexture copy.
+ encodeReadByB2TCopy(encoder: GPUCommandEncoder, srcBuffer: GPUBuffer, dstBuffer: GPUBuffer) {
+ const tmpTexture = this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'r32uint',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+
+ // The b2t copy is just encoded into command encoder, it doesn't write immediately.
+ encoder.copyBufferToTexture(
+ { buffer: srcBuffer, bytesPerRow: 256 },
+ { texture: tmpTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ // The t2b copy is just encoded into command encoder, it doesn't write immediately.
+ encoder.copyTextureToBuffer(
+ { texture: tmpTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dstBuffer, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ }
+
+ encodeReadOp(
+ helper: OperationContextHelper,
+ operation: ReadOp,
+ context: OperationContext,
+ srcBuffer: GPUBuffer,
+ dstBuffer: GPUBuffer
+ ) {
+ helper.ensureContext(context);
+
+ const renderer =
+ context === 'render-bundle-encoder' ? helper.renderBundleEncoder : helper.renderPassEncoder;
+ const computePass = context === 'compute-pass-encoder' ? helper.computePassEncoder : undefined;
+
+ switch (operation) {
+ case 'input-vertex':
+ // The srcBuffer is used as vertexBuffer.
+ // draw writes the same value in srcBuffer[0] to dstBuffer[0].
+ assert(renderer !== undefined);
+ this.encodeReadAsVertexBufferInRenderPass(renderer, srcBuffer, dstBuffer);
+ break;
+ case 'input-index':
+ // The srcBuffer is used as indexBuffer.
+ // With this vertexBuffer, drawIndexed writes the same value in srcBuffer[0] to dstBuffer[0].
+ assert(renderer !== undefined);
+ assert(this.vertexBuffer !== undefined);
+ this.encodeReadAsIndexBufferInRenderPass(renderer, srcBuffer, dstBuffer, this.vertexBuffer);
+ break;
+ case 'input-indirect':
+ // The srcBuffer is used as indirectBuffer for drawIndirect.
+ // srcBuffer[0] = 0 or 1 (vertexCount), which will decide the value written into dstBuffer to be either 0 or 1.
+ assert(renderer !== undefined);
+ assert(this.vertexBuffer !== undefined);
+ this.encodeReadAsIndirectBufferInRenderPass(
+ renderer,
+ srcBuffer,
+ dstBuffer,
+ this.vertexBuffer
+ );
+ break;
+ case 'input-indirect-index':
+ // The srcBuffer is used as indirectBuffer for drawIndexedIndirect.
+ // srcBuffer[0] = 0 or 1 (indexCount), which will decide the value written into dstBuffer to be either 0 or 1.
+ assert(renderer !== undefined);
+ assert(this.vertexBuffer !== undefined);
+ assert(this.indexBuffer !== undefined);
+ this.encodeReadAsIndexedIndirectBufferInRenderPass(
+ renderer,
+ srcBuffer,
+ dstBuffer,
+ this.vertexBuffer,
+ this.indexBuffer
+ );
+ break;
+ case 'input-indirect-dispatch':
+ // The srcBuffer is used as indirectBuffer for dispatch.
+ // srcBuffer[0] = 0 or 1 (workgroupCountX), which will decide the value written into dstBuffer to be either 0 or 1.
+ assert(computePass !== undefined);
+ this.encodeReadAsIndirectBufferInComputePass(computePass, srcBuffer, dstBuffer, 1);
+ break;
+ case 'constant-uniform':
+ // The srcBuffer is used as uniform buffer.
+ assert(renderer !== undefined);
+ this.encodeReadAsUniformBufferInRenderPass(renderer, srcBuffer, dstBuffer);
+ break;
+ case 'storage-read':
+ switch (context) {
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder':
+ assert(renderer !== undefined);
+ this.encodeReadAsStorageBufferInRenderPass(renderer, srcBuffer, dstBuffer);
+ break;
+ case 'compute-pass-encoder':
+ assert(computePass !== undefined);
+ this.encodeReadAsStorageBufferInComputePass(computePass, srcBuffer, dstBuffer);
+ break;
+ default:
+ unreachable();
+ }
+ break;
+ case 'b2b-copy':
+ assert(helper.commandEncoder !== undefined);
+ this.encodeReadByB2BCopy(helper.commandEncoder, srcBuffer, dstBuffer);
+ break;
+ case 'b2t-copy':
+ assert(helper.commandEncoder !== undefined);
+ this.encodeReadByB2TCopy(helper.commandEncoder, srcBuffer, dstBuffer);
+ break;
+ default:
+ unreachable();
+ }
+ }
+
+ verifyData(buffer: GPUBuffer, expectedValue: number) {
+ const bufferData = new Uint32Array(1);
+ bufferData[0] = expectedValue;
+ this.expectGPUBufferValuesEqual(buffer, bufferData);
+ }
+
+ verifyDataTwoValidValues(buffer: GPUBuffer, expectedValue1: number, expectedValue2: number) {
+ const bufferData1 = new Uint32Array(1);
+ bufferData1[0] = expectedValue1;
+ const bufferData2 = new Uint32Array(1);
+ bufferData2[0] = expectedValue2;
+ this.expectGPUBufferValuesPassCheck(
+ buffer,
+ a => checkElementsEqualEither(a, [bufferData1, bufferData2]),
+ { type: Uint32Array, typedLength: 1 }
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/multiple_buffers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/multiple_buffers.spec.ts
new file mode 100644
index 0000000000..b3ed842436
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/multiple_buffers.spec.ts
@@ -0,0 +1,354 @@
+export const description = `
+Memory Synchronization Tests for multiple buffers: read before write, read after write, and write after write.
+
+- Create multiple src buffers and initialize it to 0, wait on the fence to ensure the data is initialized.
+Write Op: write a value (say 1) into the src buffer via render pass, copmute pass, copy, write buffer, etc.
+Read Op: read the value from the src buffer and write it to dst buffer via render pass (vertex, index, indirect input, uniform, storage), compute pass, copy etc.
+Wait on another fence, then call expectContents to verify the dst buffer value.
+ - x= write op: {storage buffer in {compute, render, render-via-bundle}, t2b copy dst, b2b copy dst, writeBuffer}
+ - x= read op: {index buffer, vertex buffer, indirect buffer (draw, draw indexed, dispatch), uniform buffer, {readonly, readwrite} storage buffer in {compute, render, render-via-bundle}, b2b copy src, b2t copy src}
+ - x= read-write sequence: {read then write, write then read, write then write}
+ - x= op context: {queue, command-encoder, compute-pass-encoder, render-pass-encoder, render-bundle-encoder}, x= op boundary: {queue-op, command-buffer, pass, execute-bundles, render-bundle}
+ - Not every context/boundary combinations are valid. We have the checkOpsValidForContext func to do the filtering.
+ - If two writes are in the same passes, render result has loose guarantees.
+
+TODO: Tests with more than one buffer to try to stress implementations a little bit more.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import {
+ kOperationBoundaries,
+ kBoundaryInfo,
+ OperationContextHelper,
+} from '../operation_context_helper.js';
+
+import {
+ kAllReadOps,
+ kAllWriteOps,
+ BufferSyncTest,
+ checkOpsValidForContext,
+} from './buffer_sync_test.js';
+
+// The src value is what stores in the src buffer before any operation.
+const kSrcValue = 0;
+// The op value is what the read/write operation write into the target buffer.
+const kOpValue = 1;
+
+export const g = makeTestGroup(BufferSyncTest);
+
+g.test('rw')
+ .desc(
+ `
+ Perform a 'read' operations on multiple buffers, followed by a 'write' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should not see the contents written by the subsequent write.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const readOp of kAllReadOps) {
+ for (const writeOp of kAllWriteOps) {
+ if (checkOpsValidForContext([readOp, writeOp], _context)) {
+ yield {
+ readOp,
+ readContext: _context[0],
+ writeOp,
+ writeContext: _context[1],
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const srcBuffers: GPUBuffer[] = [];
+ const dstBuffers: GPUBuffer[] = [];
+
+ const kBufferCount = 4;
+ for (let i = 0; i < kBufferCount; i++) {
+ const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
+ srcBuffers.push(srcBuffer);
+ dstBuffers.push(dstBuffer);
+ }
+
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);
+
+ // The read op will read from src buffers and write to dst buffers based on what it reads.
+ // A boundary will separate multiple read and write operations. The write op will write the
+ // given op value into each src buffer as well. The write op happens after read op. So we are
+ // expecting each src value to be in the mapped dst buffer.
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeReadOp(helper, readOp, readContext, srcBuffers[i], dstBuffers[i]);
+ }
+
+ helper.ensureBoundary(boundary);
+
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeWriteOp(helper, writeOp, writeContext, srcBuffers[i], 0, kOpValue);
+ }
+
+ helper.ensureSubmit();
+
+ for (let i = 0; i < kBufferCount; i++) {
+ // Only verify the value of the first element of each dstBuffer.
+ t.verifyData(dstBuffers[i], kSrcValue);
+ }
+ });
+
+g.test('wr')
+ .desc(
+ `
+ Perform a 'write' operation on on multiple buffers, followed by a 'read' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should see exactly the contents written by the previous write.`
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const readOp of kAllReadOps) {
+ for (const writeOp of kAllWriteOps) {
+ if (checkOpsValidForContext([readOp, writeOp], _context)) {
+ yield {
+ readOp,
+ readContext: _context[0],
+ writeOp,
+ writeContext: _context[1],
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const srcBuffers: GPUBuffer[] = [];
+ const dstBuffers: GPUBuffer[] = [];
+
+ const kBufferCount = 4;
+
+ for (let i = 0; i < kBufferCount; i++) {
+ const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
+
+ srcBuffers.push(srcBuffer);
+ dstBuffers.push(dstBuffer);
+ }
+
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);
+
+ // The write op will write the given op value into src buffers.
+ // The read op will read from src buffers and write to dst buffers based on what it reads.
+ // The write op happens before read op. So we are expecting the op value to be in the dst
+ // buffers.
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeWriteOp(helper, writeOp, writeContext, srcBuffers[i], 0, kOpValue);
+ }
+
+ helper.ensureBoundary(boundary);
+
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeReadOp(helper, readOp, readContext, srcBuffers[i], dstBuffers[i]);
+ }
+
+ helper.ensureSubmit();
+
+ for (let i = 0; i < kBufferCount; i++) {
+ // Only verify the value of the first element of the dstBuffer
+ t.verifyData(dstBuffers[i], kOpValue);
+ }
+ });
+
+g.test('ww')
+ .desc(
+ `
+ Perform a 'first' write operation on multiple buffers, followed by a 'second' write operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The second write should overwrite the contents of the first.`
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const firstWriteOp of kAllWriteOps) {
+ for (const secondWriteOp of kAllWriteOps) {
+ if (checkOpsValidForContext([firstWriteOp, secondWriteOp], _context)) {
+ yield {
+ writeOps: [firstWriteOp, secondWriteOp],
+ contexts: _context,
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { writeOps, contexts, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const buffers: GPUBuffer[] = [];
+
+ const kBufferCount = 4;
+
+ for (let i = 0; i < kBufferCount; i++) {
+ const buffer = await t.createBufferWithValue(0);
+
+ buffers.push(buffer);
+ }
+
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[0], 0, 1);
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[1], 1, 2);
+
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeWriteOp(helper, writeOps[0], contexts[0], buffers[i], 0, 1);
+ }
+
+ helper.ensureBoundary(boundary);
+
+ for (let i = 0; i < kBufferCount; i++) {
+ t.encodeWriteOp(helper, writeOps[1], contexts[1], buffers[i], 1, 2);
+ }
+
+ helper.ensureSubmit();
+
+ for (let i = 0; i < kBufferCount; i++) {
+ t.verifyData(buffers[i], 2);
+ }
+ });
+
+g.test('multiple_pairs_of_draws_in_one_render_pass')
+ .desc(
+ `
+ Test write-after-write operations on multiple buffers via the one render pass. The first write
+ will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
+ buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is either
+ buffer index * 2 + 1 or buffer index * 2 + 2. It may use bundle in each draw.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('firstDrawUseBundle', [false, true])
+ .combine('secondDrawUseBundle', [false, true])
+ )
+ .fn(async t => {
+ const { firstDrawUseBundle, secondDrawUseBundle } = t.params;
+
+ const encoder = t.device.createCommandEncoder();
+ const passEncoder = t.beginSimpleRenderPass(encoder);
+
+ const kBufferCount = 4;
+ const buffers: GPUBuffer[] = [];
+ for (let b = 0; b < kBufferCount; ++b) {
+ const buffer = await t.createBufferWithValue(0);
+ buffers.push(buffer);
+
+ const useBundle = [firstDrawUseBundle, secondDrawUseBundle];
+ for (let i = 0; i < 2; ++i) {
+ const renderEncoder = useBundle[i]
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ })
+ : passEncoder;
+ const pipeline = t.createStorageWriteRenderPipeline(2 * b + i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ renderEncoder.setPipeline(pipeline);
+ renderEncoder.setBindGroup(0, bindGroup);
+ renderEncoder.draw(1, 1, 0, 0);
+ if (useBundle[i])
+ passEncoder.executeBundles([(renderEncoder as GPURenderBundleEncoder).finish()]);
+ }
+ }
+
+ passEncoder.end();
+ t.device.queue.submit([encoder.finish()]);
+ for (let b = 0; b < kBufferCount; ++b) {
+ t.verifyDataTwoValidValues(buffers[b], 2 * b + 1, 2 * b + 2);
+ }
+ });
+
+g.test('multiple_pairs_of_draws_in_one_render_bundle')
+ .desc(
+ `
+ Test write-after-write operations on multiple buffers via the one render bundle. The first write
+ will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
+ buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is either
+ buffer index * 2 + 1 or buffer index * 2 + 2.
+ `
+ )
+ .fn(async t => {
+ const encoder = t.device.createCommandEncoder();
+ const passEncoder = t.beginSimpleRenderPass(encoder);
+ const renderEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ });
+
+ const kBufferCount = 4;
+ const buffers: GPUBuffer[] = [];
+ for (let b = 0; b < kBufferCount; ++b) {
+ const buffer = await t.createBufferWithValue(0);
+ buffers.push(buffer);
+
+ for (let i = 0; i < 2; ++i) {
+ const pipeline = t.createStorageWriteRenderPipeline(2 * b + i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ renderEncoder.setPipeline(pipeline);
+ renderEncoder.setBindGroup(0, bindGroup);
+ renderEncoder.draw(1, 1, 0, 0);
+ }
+ }
+
+ passEncoder.executeBundles([renderEncoder.finish()]);
+ passEncoder.end();
+ t.device.queue.submit([encoder.finish()]);
+ for (let b = 0; b < kBufferCount; ++b) {
+ t.verifyDataTwoValidValues(buffers[b], 2 * b + 1, 2 * b + 2);
+ }
+ });
+
+g.test('multiple_pairs_of_dispatches_in_one_compute_pass')
+ .desc(
+ `
+ Test write-after-write operations on multiple buffers via the one compute pass. The first write
+ will write the buffer index * 2 + 1 into all storage buffers. The second write will write the
+ buffer index * 2 + 2 into the all buffers in the same pass. Expected data in all buffers is the
+ buffer index * 2 + 2.
+ `
+ )
+ .fn(async t => {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+
+ const kBufferCount = 4;
+ const buffers: GPUBuffer[] = [];
+ for (let b = 0; b < kBufferCount; ++b) {
+ const buffer = await t.createBufferWithValue(0);
+ buffers.push(buffer);
+
+ for (let i = 0; i < 2; ++i) {
+ const pipeline = t.createStorageWriteComputePipeline(2 * b + i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ }
+ }
+
+ pass.end();
+
+ t.device.queue.submit([encoder.finish()]);
+ for (let b = 0; b < kBufferCount; ++b) {
+ t.verifyData(buffers[b], 2 * b + 2);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/single_buffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/single_buffer.spec.ts
new file mode 100644
index 0000000000..e05a441dc3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/buffer/single_buffer.spec.ts
@@ -0,0 +1,257 @@
+export const description = `
+Memory Synchronization Tests for Buffer: read before write, read after write, and write after write.
+
+- Create a src buffer and initialize it to 0, wait on the fence to ensure the data is initialized.
+Write Op: write a value (say 1) into the src buffer via render pass, copmute pass, copy, write buffer, etc.
+Read Op: read the value from the src buffer and write it to dst buffer via render pass (vertex, index, indirect input, uniform, storage), compute pass, copy etc.
+Wait on another fence, then call expectContents to verify the dst buffer value.
+ - x= write op: {storage buffer in {compute, render, render-via-bundle}, t2b copy dst, b2b copy dst, writeBuffer}
+ - x= read op: {index buffer, vertex buffer, indirect buffer (draw, draw indexed, dispatch), uniform buffer, {readonly, readwrite} storage buffer in {compute, render, render-via-bundle}, b2b copy src, b2t copy src}
+ - x= read-write sequence: {read then write, write then read, write then write}
+ - x= op context: {queue, command-encoder, compute-pass-encoder, render-pass-encoder, render-bundle-encoder}, x= op boundary: {queue-op, command-buffer, pass, execute-bundles, render-bundle}
+ - Not every context/boundary combinations are valid. We have the checkOpsValidForContext func to do the filtering.
+ - If two writes are in the same passes, render result has loose guarantees.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import {
+ kOperationBoundaries,
+ kBoundaryInfo,
+ OperationContextHelper,
+} from '../operation_context_helper.js';
+
+import {
+ kAllReadOps,
+ kAllWriteOps,
+ BufferSyncTest,
+ checkOpsValidForContext,
+} from './buffer_sync_test.js';
+
+// The src value is what stores in the src buffer before any operation.
+const kSrcValue = 0;
+// The op value is what the read/write operation write into the target buffer.
+const kOpValue = 1;
+
+export const g = makeTestGroup(BufferSyncTest);
+
+g.test('rw')
+ .desc(
+ `
+ Perform a 'read' operations on a buffer, followed by a 'write' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should not see the contents written by the subsequent write.`
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const readOp of kAllReadOps) {
+ for (const writeOp of kAllWriteOps) {
+ if (checkOpsValidForContext([readOp, writeOp], _context)) {
+ yield {
+ readOp,
+ readContext: _context[0],
+ writeOp,
+ writeContext: _context[1],
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);
+
+ // The read op will read from src buffer and write to dst buffer based on what it reads.
+ // The write op will write the given op value into src buffer as well.
+ // The write op happens after read op. So we are expecting the src value to be in the dst buffer.
+ t.encodeReadOp(helper, readOp, readContext, srcBuffer, dstBuffer);
+ helper.ensureBoundary(boundary);
+ t.encodeWriteOp(helper, writeOp, writeContext, srcBuffer, 0, kOpValue);
+ helper.ensureSubmit();
+ // Only verify the value of the first element of the dstBuffer
+ t.verifyData(dstBuffer, kSrcValue);
+ });
+
+g.test('wr')
+ .desc(
+ `
+ Perform a 'write' operation on a buffer, followed by a 'read' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should see exactly the contents written by the previous write.`
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const readOp of kAllReadOps) {
+ for (const writeOp of kAllWriteOps) {
+ if (checkOpsValidForContext([readOp, writeOp], _context)) {
+ yield {
+ readOp,
+ readContext: _context[0],
+ writeOp,
+ writeContext: _context[1],
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { readContext, readOp, writeContext, writeOp, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const { srcBuffer, dstBuffer } = await t.createBuffersForReadOp(readOp, kSrcValue, kOpValue);
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOp, 0, kOpValue);
+
+ // The write op will write the given op value into src buffer.
+ // The read op will read from src buffer and write to dst buffer based on what it reads.
+ // The write op happens before read op. So we are expecting the op value to be in the dst buffer.
+ t.encodeWriteOp(helper, writeOp, writeContext, srcBuffer, 0, kOpValue);
+ helper.ensureBoundary(boundary);
+ t.encodeReadOp(helper, readOp, readContext, srcBuffer, dstBuffer);
+ helper.ensureSubmit();
+ // Only verify the value of the first element of the dstBuffer
+ t.verifyData(dstBuffer, kOpValue);
+ });
+
+g.test('ww')
+ .desc(
+ `
+ Perform a 'first' write operation on a buffer, followed by a 'second' write operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The second write should overwrite the contents of the first.`
+ )
+ .params(u =>
+ u //
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const firstWriteOp of kAllWriteOps) {
+ for (const secondWriteOp of kAllWriteOps) {
+ if (checkOpsValidForContext([firstWriteOp, secondWriteOp], _context)) {
+ yield {
+ writeOps: [firstWriteOp, secondWriteOp],
+ contexts: _context,
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(async t => {
+ const { writeOps, contexts, boundary } = t.params;
+ const helper = new OperationContextHelper(t);
+
+ const buffer = await t.createBufferWithValue(0);
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[0], 0, 1);
+ await t.createIntermediateBuffersAndTexturesForWriteOp(writeOps[1], 1, 2);
+
+ t.encodeWriteOp(helper, writeOps[0], contexts[0], buffer, 0, 1);
+ helper.ensureBoundary(boundary);
+ t.encodeWriteOp(helper, writeOps[1], contexts[1], buffer, 1, 2);
+ helper.ensureSubmit();
+ t.verifyData(buffer, 2);
+ });
+
+// Cases with loose render result guarentees.
+
+g.test('two_draws_in_the_same_render_pass')
+ .desc(
+ `Test write-after-write operations in the same render pass. The first write will write 1 into
+ a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
+ data in buffer is either 1 or 2. It may use bundle in each draw.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('firstDrawUseBundle', [false, true])
+ .combine('secondDrawUseBundle', [false, true])
+ )
+ .fn(async t => {
+ const { firstDrawUseBundle, secondDrawUseBundle } = t.params;
+ const buffer = await t.createBufferWithValue(0);
+ const encoder = t.device.createCommandEncoder();
+ const passEncoder = t.beginSimpleRenderPass(encoder);
+
+ const useBundle = [firstDrawUseBundle, secondDrawUseBundle];
+ for (let i = 0; i < 2; ++i) {
+ const renderEncoder = useBundle[i]
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ })
+ : passEncoder;
+ const pipeline = t.createStorageWriteRenderPipeline(i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ renderEncoder.setPipeline(pipeline);
+ renderEncoder.setBindGroup(0, bindGroup);
+ renderEncoder.draw(1, 1, 0, 0);
+ if (useBundle[i])
+ passEncoder.executeBundles([(renderEncoder as GPURenderBundleEncoder).finish()]);
+ }
+
+ passEncoder.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.verifyDataTwoValidValues(buffer, 1, 2);
+ });
+
+g.test('two_draws_in_the_same_render_bundle')
+ .desc(
+ `Test write-after-write operations in the same render bundle. The first write will write 1 into
+ a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
+ data in buffer is either 1 or 2.`
+ )
+ .fn(async t => {
+ const buffer = await t.createBufferWithValue(0);
+ const encoder = t.device.createCommandEncoder();
+ const passEncoder = t.beginSimpleRenderPass(encoder);
+ const renderEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ });
+
+ for (let i = 0; i < 2; ++i) {
+ const pipeline = t.createStorageWriteRenderPipeline(i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ renderEncoder.setPipeline(pipeline);
+ renderEncoder.setBindGroup(0, bindGroup);
+ renderEncoder.draw(1, 1, 0, 0);
+ }
+
+ passEncoder.executeBundles([renderEncoder.finish()]);
+ passEncoder.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.verifyDataTwoValidValues(buffer, 1, 2);
+ });
+
+g.test('two_dispatches_in_the_same_compute_pass')
+ .desc(
+ `Test write-after-write operations in the same compute pass. The first write will write 1 into
+ a storage buffer. The second write will write 2 into the same buffer in the same pass. Expected
+ data in buffer is 2.`
+ )
+ .fn(async t => {
+ const buffer = await t.createBufferWithValue(0);
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+
+ for (let i = 0; i < 2; ++i) {
+ const pipeline = t.createStorageWriteComputePipeline(i + 1);
+ const bindGroup = t.createBindGroup(pipeline, buffer);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ }
+
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ t.verifyData(buffer, 2);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/operation_context_helper.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/operation_context_helper.ts
new file mode 100644
index 0000000000..f095969e0d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/operation_context_helper.ts
@@ -0,0 +1,334 @@
+import { assert, unreachable } from '../../../../common/util/util.js';
+import { EncodableTextureFormat } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+/**
+ * Boundary between the first operation, and the second operation.
+ */
+export const kOperationBoundaries = [
+ 'queue-op', // Operations are performed in different queue operations (submit, writeTexture).
+ 'command-buffer', // Operations are in different command buffers.
+ 'pass', // Operations are in different passes.
+ 'execute-bundles', // Operations are in different executeBundles(...) calls
+ 'render-bundle', // Operations are in different render bundles.
+ 'dispatch', // Operations are in different dispatches.
+ 'draw', // Operations are in different draws.
+] as const;
+export type OperationBoundary = typeof kOperationBoundaries[number];
+
+/**
+ * Context a particular operation is permitted in.
+ * These contexts should be sorted such that the first is the most top-level
+ * context, and the last is most nested (inside a render bundle, in a render pass, ...).
+ */
+export const kOperationContexts = [
+ 'queue', // Operation occurs on the GPUQueue object
+ 'command-encoder', // Operation may be encoded in a GPUCommandEncoder.
+ 'compute-pass-encoder', // Operation may be encoded in a GPUComputePassEncoder.
+ 'render-pass-encoder', // Operation may be encoded in a GPURenderPassEncoder.
+ 'render-bundle-encoder', // Operation may be encoded in a GPURenderBundleEncoder.
+] as const;
+export type OperationContext = typeof kOperationContexts[number];
+
+interface BoundaryInfo {
+ readonly contexts: [OperationContext, OperationContext][];
+ // Add fields as needed
+}
+
+function combineContexts(
+ as: readonly OperationContext[],
+ bs: readonly OperationContext[]
+): [OperationContext, OperationContext][] {
+ const result: [OperationContext, OperationContext][] = [];
+ for (const a of as) {
+ for (const b of bs) {
+ result.push([a, b]);
+ }
+ }
+ return result;
+}
+
+const queueContexts = combineContexts(kOperationContexts, kOperationContexts);
+const commandBufferContexts = combineContexts(
+ kOperationContexts.filter(c => c !== 'queue'),
+ kOperationContexts.filter(c => c !== 'queue')
+);
+
+/**
+ * Mapping of OperationBoundary => to a set of OperationContext pairs.
+ * The boundary is capable of separating operations in those two contexts.
+ */
+export const kBoundaryInfo: {
+ readonly [k in OperationBoundary]: BoundaryInfo;
+} = /* prettier-ignore */ {
+ 'queue-op': {
+ contexts: queueContexts,
+ },
+ 'command-buffer': {
+ contexts: commandBufferContexts,
+ },
+ 'pass': {
+ contexts: [
+ ['compute-pass-encoder', 'compute-pass-encoder'],
+ ['compute-pass-encoder', 'render-pass-encoder'],
+ ['render-pass-encoder', 'compute-pass-encoder'],
+ ['render-pass-encoder', 'render-pass-encoder'],
+ ['render-bundle-encoder', 'render-pass-encoder'],
+ ['render-pass-encoder', 'render-bundle-encoder'],
+ ['render-bundle-encoder', 'render-bundle-encoder'],
+ ],
+ },
+ 'execute-bundles': {
+ contexts: [
+ ['render-bundle-encoder', 'render-bundle-encoder'],
+ ]
+ },
+ 'render-bundle': {
+ contexts: [
+ ['render-bundle-encoder', 'render-pass-encoder'],
+ ['render-pass-encoder', 'render-bundle-encoder'],
+ ['render-bundle-encoder', 'render-bundle-encoder'],
+ ],
+ },
+ 'dispatch': {
+ contexts: [
+ ['compute-pass-encoder', 'compute-pass-encoder'],
+ ],
+ },
+ 'draw': {
+ contexts: [
+ ['render-pass-encoder', 'render-pass-encoder'],
+ ['render-bundle-encoder', 'render-pass-encoder'],
+ ['render-pass-encoder', 'render-bundle-encoder'],
+ ],
+ },
+};
+
+export class OperationContextHelper {
+ // We start at the queue context which is top-level.
+ protected currentContext: OperationContext = 'queue';
+
+ // Set based on the current context.
+ queue: GPUQueue;
+ commandEncoder?: GPUCommandEncoder;
+ computePassEncoder?: GPUComputePassEncoder;
+ renderPassEncoder?: GPURenderPassEncoder;
+ renderBundleEncoder?: GPURenderBundleEncoder;
+
+ protected t: GPUTest;
+ protected device: GPUDevice;
+
+ protected commandBuffers: GPUCommandBuffer[] = [];
+ protected renderBundles: GPURenderBundle[] = [];
+
+ public readonly kTextureSize = [4, 4] as const;
+ public readonly kTextureFormat: EncodableTextureFormat = 'rgba8unorm';
+
+ constructor(t: GPUTest) {
+ this.t = t;
+ this.device = t.device;
+ this.queue = t.device.queue;
+ }
+
+ // Ensure that all encoded commands are finished and submitted.
+ ensureSubmit() {
+ this.ensureContext('queue');
+ this.flushCommandBuffers();
+ }
+
+ private popContext(): GPURenderBundle | GPUCommandBuffer | null {
+ switch (this.currentContext) {
+ case 'queue':
+ unreachable();
+ break;
+ case 'command-encoder': {
+ assert(this.commandEncoder !== undefined);
+ const commandBuffer = this.commandEncoder.finish();
+ this.commandEncoder = undefined;
+ this.currentContext = 'queue';
+ return commandBuffer;
+ }
+ case 'compute-pass-encoder':
+ assert(this.computePassEncoder !== undefined);
+ this.computePassEncoder.end();
+ this.computePassEncoder = undefined;
+ this.currentContext = 'command-encoder';
+ break;
+ case 'render-pass-encoder':
+ assert(this.renderPassEncoder !== undefined);
+ this.renderPassEncoder.end();
+ this.renderPassEncoder = undefined;
+ this.currentContext = 'command-encoder';
+ break;
+ case 'render-bundle-encoder': {
+ assert(this.renderBundleEncoder !== undefined);
+ const renderBundle = this.renderBundleEncoder.finish();
+ this.renderBundleEncoder = undefined;
+ this.currentContext = 'render-pass-encoder';
+ return renderBundle;
+ }
+ }
+ return null;
+ }
+
+ private makeDummyAttachment(): GPURenderPassColorAttachment {
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ format: this.kTextureFormat,
+ size: this.kTextureSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ );
+ return {
+ view: texture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ };
+ }
+
+ ensureContext(context: OperationContext) {
+ // Find the common ancestor. So we can transition from currentContext -> context.
+ const ancestorContext =
+ kOperationContexts[
+ Math.min(
+ kOperationContexts.indexOf(context),
+ kOperationContexts.indexOf(this.currentContext)
+ )
+ ];
+
+ // Pop the context until we're at the common ancestor.
+ while (this.currentContext !== ancestorContext) {
+ // About to pop the render pass encoder. Execute any outstanding render bundles.
+ if (this.currentContext === 'render-pass-encoder') {
+ this.flushRenderBundles();
+ }
+
+ const result = this.popContext();
+ if (result) {
+ if (result instanceof GPURenderBundle) {
+ this.renderBundles.push(result);
+ } else {
+ this.commandBuffers.push(result);
+ }
+ }
+ }
+
+ if (this.currentContext === context) {
+ return;
+ }
+
+ switch (context) {
+ case 'queue':
+ unreachable();
+ break;
+ case 'command-encoder':
+ assert(this.currentContext === 'queue');
+ this.commandEncoder = this.device.createCommandEncoder();
+ break;
+ case 'compute-pass-encoder':
+ switch (this.currentContext) {
+ case 'queue':
+ this.commandEncoder = this.device.createCommandEncoder();
+ // fallthrough
+ case 'command-encoder':
+ assert(this.commandEncoder !== undefined);
+ this.computePassEncoder = this.commandEncoder.beginComputePass();
+ break;
+ case 'compute-pass-encoder':
+ case 'render-bundle-encoder':
+ case 'render-pass-encoder':
+ unreachable();
+ }
+ break;
+ case 'render-pass-encoder':
+ switch (this.currentContext) {
+ case 'queue':
+ this.commandEncoder = this.device.createCommandEncoder();
+ // fallthrough
+ case 'command-encoder':
+ assert(this.commandEncoder !== undefined);
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [this.makeDummyAttachment()],
+ });
+ break;
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder':
+ case 'compute-pass-encoder':
+ unreachable();
+ }
+ break;
+ case 'render-bundle-encoder':
+ switch (this.currentContext) {
+ case 'queue':
+ this.commandEncoder = this.device.createCommandEncoder();
+ // fallthrough
+ case 'command-encoder':
+ assert(this.commandEncoder !== undefined);
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [this.makeDummyAttachment()],
+ });
+ // fallthrough
+ case 'render-pass-encoder':
+ this.renderBundleEncoder = this.device.createRenderBundleEncoder({
+ colorFormats: [this.kTextureFormat],
+ });
+ break;
+ case 'render-bundle-encoder':
+ case 'compute-pass-encoder':
+ unreachable();
+ }
+ break;
+ }
+ this.currentContext = context;
+ }
+
+ private flushRenderBundles() {
+ assert(this.renderPassEncoder !== undefined);
+ if (this.renderBundles.length) {
+ this.renderPassEncoder.executeBundles(this.renderBundles);
+ this.renderBundles = [];
+ }
+ }
+
+ private flushCommandBuffers() {
+ if (this.commandBuffers.length) {
+ this.queue.submit(this.commandBuffers);
+ this.commandBuffers = [];
+ }
+ }
+
+ ensureBoundary(boundary: OperationBoundary) {
+ switch (boundary) {
+ case 'command-buffer':
+ this.ensureContext('queue');
+ break;
+ case 'queue-op':
+ this.ensureContext('queue');
+ // Submit any GPUCommandBuffers so the next one is in a separate submit.
+ this.flushCommandBuffers();
+ break;
+ case 'dispatch':
+ // Nothing to do to separate dispatches.
+ assert(this.currentContext === 'compute-pass-encoder');
+ break;
+ case 'draw':
+ // Nothing to do to separate draws.
+ assert(
+ this.currentContext === 'render-pass-encoder' ||
+ this.currentContext === 'render-bundle-encoder'
+ );
+ break;
+ case 'pass':
+ this.ensureContext('command-encoder');
+ break;
+ case 'render-bundle':
+ this.ensureContext('render-pass-encoder');
+ break;
+ case 'execute-bundles':
+ this.ensureContext('render-pass-encoder');
+ // Execute any GPURenderBundles so the next one is in a separate executeBundles.
+ this.flushRenderBundles();
+ break;
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts
new file mode 100644
index 0000000000..38b5bf3bcc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/same_subresource.spec.ts
@@ -0,0 +1,709 @@
+export const description = `
+Memory Synchronization Tests for Texture: read before write, read after write, and write after write to the same subresource.
+
+- TODO: Test synchronization between multiple queues.
+- TODO: Test depth/stencil attachments.
+- TODO: Use non-solid-color texture contents [2]
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { assert, memcpy, unreachable } from '../../../../../common/util/util.js';
+import { EncodableTextureFormat } from '../../../../capability_info.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { align } from '../../../../util/math.js';
+import { getTextureCopyLayout } from '../../../../util/texture/layout.js';
+import {
+ kTexelRepresentationInfo,
+ PerTexelComponent,
+} from '../../../../util/texture/texel_data.js';
+import {
+ kOperationBoundaries,
+ OperationContext,
+ kBoundaryInfo,
+ OperationContextHelper,
+} from '../operation_context_helper.js';
+
+import {
+ kAllReadOps,
+ kAllWriteOps,
+ checkOpsValidForContext,
+ Op,
+ kOpInfo,
+} from './texture_sync_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const fullscreenQuadWGSL = `
+ struct VertexOutput {
+ @builtin(position) Position : vec4<f32>
+ };
+
+ @vertex fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ return output;
+ }
+`;
+
+class TextureSyncTestHelper extends OperationContextHelper {
+ private texture: GPUTexture;
+
+ public readonly kTextureSize = [4, 4] as const;
+ public readonly kTextureFormat: EncodableTextureFormat = 'rgba8unorm';
+
+ constructor(
+ t: GPUTest,
+ textureCreationParams: {
+ usage: GPUTextureUsageFlags;
+ }
+ ) {
+ super(t);
+ this.texture = t.trackForCleanup(
+ t.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ ...textureCreationParams,
+ })
+ );
+ }
+
+ /**
+ * Perform a read operation on the test texture.
+ * @return GPUTexture copy containing the contents.
+ */
+ performReadOp({ op, in: context }: { op: Op; in: OperationContext }): GPUTexture {
+ this.ensureContext(context);
+ switch (op) {
+ case 't2t-copy': {
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToTexture(
+ {
+ texture: this.texture,
+ },
+ { texture },
+ this.kTextureSize
+ );
+ return texture;
+ }
+ case 't2b-copy': {
+ const { byteLength, bytesPerRow } = getTextureCopyLayout(this.kTextureFormat, '2d', [
+ ...this.kTextureSize,
+ 1,
+ ]);
+ const buffer = this.t.trackForCleanup(
+ this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ })
+ );
+
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ })
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToBuffer(
+ {
+ texture: this.texture,
+ },
+ { buffer, bytesPerRow },
+ this.kTextureSize
+ );
+ this.commandEncoder.copyBufferToTexture(
+ { buffer, bytesPerRow },
+ { texture },
+ this.kTextureSize
+ );
+ return texture;
+ }
+ case 'sample': {
+ const texture = this.t.trackForCleanup(
+ this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
+ })
+ );
+
+ const bindGroupLayout = this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ texture: {
+ sampleType: 'unfilterable-float',
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: 'write-only',
+ format: this.kTextureFormat,
+ },
+ },
+ ],
+ });
+
+ const bindGroup = this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: this.texture.createView(),
+ },
+ {
+ binding: 1,
+ resource: texture.createView(),
+ },
+ ],
+ });
+
+ switch (context) {
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder': {
+ const module = this.device.createShaderModule({
+ code: `${fullscreenQuadWGSL}
+
+ @group(0) @binding(0) var inputTex: texture_2d<f32>;
+ @group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @fragment fn frag_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
+ let coord = vec2<i32>(fragCoord.xy);
+ textureStore(outputTex, coord, textureLoad(inputTex, coord, 0));
+ return vec4<f32>();
+ }
+ `,
+ });
+ const renderPipeline = this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+
+ // Unused attachment since we can't use textureStore in the vertex shader.
+ // Set writeMask to zero.
+ targets: [
+ {
+ format: this.kTextureFormat,
+ writeMask: 0,
+ },
+ ],
+ },
+ });
+
+ switch (context) {
+ case 'render-bundle-encoder':
+ assert(this.renderBundleEncoder !== undefined);
+ this.renderBundleEncoder.setPipeline(renderPipeline);
+ this.renderBundleEncoder.setBindGroup(0, bindGroup);
+ this.renderBundleEncoder.draw(6);
+ break;
+ case 'render-pass-encoder':
+ assert(this.renderPassEncoder !== undefined);
+ this.renderPassEncoder.setPipeline(renderPipeline);
+ this.renderPassEncoder.setBindGroup(0, bindGroup);
+ this.renderPassEncoder.draw(6);
+ break;
+ }
+ break;
+ }
+ case 'compute-pass-encoder': {
+ const module = this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var inputTex: texture_2d<f32>;
+ @group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(8, 8)
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
+ if (any(gid.xy >= vec2<u32>(textureDimensions(inputTex)))) {
+ return;
+ }
+ let coord = vec2<i32>(gid.xy);
+ textureStore(outputTex, coord, textureLoad(inputTex, coord, 0));
+ }
+ `,
+ });
+ const computePipeline = this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ });
+
+ assert(this.computePassEncoder !== undefined);
+ this.computePassEncoder.setPipeline(computePipeline);
+ this.computePassEncoder.setBindGroup(0, bindGroup);
+ this.computePassEncoder.dispatchWorkgroups(
+ Math.ceil(this.kTextureSize[0] / 8),
+ Math.ceil(this.kTextureSize[1] / 8)
+ );
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ return texture;
+ }
+ case 'b2t-copy':
+ case 'attachment-resolve':
+ case 'attachment-store':
+ unreachable();
+ }
+ unreachable();
+ }
+
+ performWriteOp(
+ { op, in: context }: { op: Op; in: OperationContext },
+ data: PerTexelComponent<number>
+ ) {
+ this.ensureContext(context);
+ switch (op) {
+ case 'attachment-store': {
+ assert(this.commandEncoder !== undefined);
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: this.texture.createView(),
+ // [2] Use non-solid-color texture values
+ clearValue: [data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ this.currentContext = 'render-pass-encoder';
+ break;
+ }
+ case 'write-texture': {
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const numTexels = this.kTextureSize[0] * this.kTextureSize[1];
+ const fullTexelData = new ArrayBuffer(texelData.byteLength * numTexels);
+ for (let i = 0; i < numTexels; ++i) {
+ memcpy({ src: texelData }, { dst: fullTexelData, start: i * texelData.byteLength });
+ }
+
+ this.queue.writeTexture(
+ { texture: this.texture },
+ fullTexelData,
+ {
+ bytesPerRow: texelData.byteLength * this.kTextureSize[0],
+ },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 't2t-copy': {
+ const texture = this.device.createTexture({
+ size: this.kTextureSize,
+ format: this.kTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const numTexels = this.kTextureSize[0] * this.kTextureSize[1];
+ const fullTexelData = new ArrayBuffer(texelData.byteLength * numTexels);
+ for (let i = 0; i < numTexels; ++i) {
+ memcpy({ src: texelData }, { dst: fullTexelData, start: i * texelData.byteLength });
+ }
+
+ this.queue.writeTexture(
+ { texture },
+ fullTexelData,
+ {
+ bytesPerRow: texelData.byteLength * this.kTextureSize[0],
+ },
+ this.kTextureSize
+ );
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyTextureToTexture(
+ { texture },
+ { texture: this.texture },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 'b2t-copy': {
+ // [2] Use non-solid-color texture values
+ const rep = kTexelRepresentationInfo[this.kTextureFormat];
+ const texelData = rep.pack(rep.encode(data));
+ const bytesPerRow = align(texelData.byteLength, 256);
+ const fullTexelData = new ArrayBuffer(bytesPerRow * this.kTextureSize[1]);
+ for (let i = 0; i < this.kTextureSize[1]; ++i) {
+ for (let j = 0; j < this.kTextureSize[0]; ++j) {
+ memcpy(
+ { src: texelData },
+ {
+ dst: fullTexelData,
+ start: i * bytesPerRow + j * texelData.byteLength,
+ }
+ );
+ }
+ }
+
+ const buffer = this.t.trackForCleanup(
+ this.device.createBuffer({
+ size: fullTexelData.byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ })
+ );
+
+ this.queue.writeBuffer(buffer, 0, fullTexelData);
+
+ assert(this.commandEncoder !== undefined);
+ this.commandEncoder.copyBufferToTexture(
+ { buffer, bytesPerRow },
+ { texture: this.texture },
+ this.kTextureSize
+ );
+ break;
+ }
+ case 'attachment-resolve': {
+ assert(this.commandEncoder !== undefined);
+ const renderTarget = this.t.trackForCleanup(
+ this.device.createTexture({
+ format: this.kTextureFormat,
+ size: this.kTextureSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: 4,
+ })
+ );
+ this.renderPassEncoder = this.commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ resolveTarget: this.texture.createView(),
+ // [2] Use non-solid-color texture values
+ clearValue: [data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0],
+ loadOp: 'clear',
+ storeOp: 'discard',
+ },
+ ],
+ });
+ this.currentContext = 'render-pass-encoder';
+ break;
+ }
+ case 'storage': {
+ const bindGroupLayout = this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: 'write-only',
+ format: this.kTextureFormat,
+ },
+ },
+ ],
+ });
+
+ const bindGroup = this.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: this.texture.createView(),
+ },
+ ],
+ });
+
+ // [2] Use non-solid-color texture values
+ const storedValue = `vec4<f32>(${[data.R ?? 0, data.G ?? 0, data.B ?? 0, data.A ?? 0]
+ .map(x => x.toFixed(5))
+ .join(', ')})`;
+
+ switch (context) {
+ case 'render-pass-encoder':
+ case 'render-bundle-encoder': {
+ const module = this.device.createShaderModule({
+ code: `${fullscreenQuadWGSL}
+
+ @group(0) @binding(0) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @fragment fn frag_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
+ textureStore(outputTex, vec2<i32>(fragCoord.xy), ${storedValue});
+ return vec4<f32>();
+ }
+ `,
+ });
+ const renderPipeline = this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+
+ // Unused attachment since we can't use textureStore in the vertex shader.
+ // Set writeMask to zero.
+ targets: [
+ {
+ format: this.kTextureFormat,
+ writeMask: 0,
+ },
+ ],
+ },
+ });
+
+ switch (context) {
+ case 'render-bundle-encoder':
+ assert(this.renderBundleEncoder !== undefined);
+ this.renderBundleEncoder.setPipeline(renderPipeline);
+ this.renderBundleEncoder.setBindGroup(0, bindGroup);
+ this.renderBundleEncoder.draw(6);
+ break;
+ case 'render-pass-encoder':
+ assert(this.renderPassEncoder !== undefined);
+ this.renderPassEncoder.setPipeline(renderPipeline);
+ this.renderPassEncoder.setBindGroup(0, bindGroup);
+ this.renderPassEncoder.draw(6);
+ break;
+ }
+ break;
+ }
+ case 'compute-pass-encoder': {
+ const module = this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var outputTex: texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(8, 8)
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
+ if (any(gid.xy >= vec2<u32>(textureDimensions(outputTex)))) {
+ return;
+ }
+ let coord = vec2<i32>(gid.xy);
+ textureStore(outputTex, coord, ${storedValue});
+ }
+ `,
+ });
+ const computePipeline = this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ }),
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ });
+
+ assert(this.computePassEncoder !== undefined);
+ this.computePassEncoder.setPipeline(computePipeline);
+ this.computePassEncoder.setBindGroup(0, bindGroup);
+ this.computePassEncoder.dispatchWorkgroups(
+ Math.ceil(this.kTextureSize[0] / 8),
+ Math.ceil(this.kTextureSize[1] / 8)
+ );
+ break;
+ }
+ default:
+ unreachable();
+ }
+ break;
+ }
+ case 't2b-copy':
+ case 'sample':
+ unreachable();
+ }
+ }
+}
+
+g.test('rw')
+ .desc(
+ `
+ Perform a 'read' operations on a texture subresource, followed by a 'write' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should not see the contents written by the subsequent write.`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const read of kAllReadOps) {
+ for (const write of kAllWriteOps) {
+ if (checkOpsValidForContext([read, write], _context)) {
+ yield {
+ read: { op: read, in: _context[0] },
+ write: { op: write, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage:
+ GPUTextureUsage.COPY_DST |
+ kOpInfo[t.params.read.op].readUsage |
+ kOpInfo[t.params.write.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue1 = { R: 0, G: 1, B: 0, A: 1 } as const;
+ const texelValue2 = { R: 1, G: 0, B: 0, A: 1 } as const;
+
+ // Initialize the texture with something.
+ helper.performWriteOp({ op: 'write-texture', in: 'queue' }, texelValue1);
+ const readbackTexture = helper.performReadOp(t.params.read);
+ helper.ensureBoundary(t.params.boundary);
+ helper.performWriteOp(t.params.write, texelValue2);
+ helper.ensureSubmit();
+
+ // Contents should be the first value written, not the second.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue1,
+ });
+ });
+
+g.test('wr')
+ .desc(
+ `
+ Perform a 'write' operation on a texture subresource, followed by a 'read' operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The read should see exactly the contents written by the previous write.
+
+ - TODO: Use non-solid-color texture contents [2]`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const read of kAllReadOps) {
+ for (const write of kAllWriteOps) {
+ if (checkOpsValidForContext([write, read], _context)) {
+ yield {
+ write: { op: write, in: _context[0] },
+ read: { op: read, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage: kOpInfo[t.params.read.op].readUsage | kOpInfo[t.params.write.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue = { R: 0, G: 1, B: 0, A: 1 } as const;
+
+ helper.performWriteOp(t.params.write, texelValue);
+ helper.ensureBoundary(t.params.boundary);
+ const readbackTexture = helper.performReadOp(t.params.read);
+ helper.ensureSubmit();
+
+ // Contents should be exactly the values written.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue,
+ });
+ });
+
+g.test('ww')
+ .desc(
+ `
+ Perform a 'first' write operation on a texture subresource, followed by a 'second' write operation.
+ Operations are separated by a 'boundary' (pass, encoder, queue-op, etc.).
+ Test that the results are synchronized.
+ The second write should overwrite the contents of the first.`
+ )
+ .params(u =>
+ u
+ .combine('boundary', kOperationBoundaries)
+ .expand('_context', p => kBoundaryInfo[p.boundary].contexts)
+ .expandWithParams(function* ({ _context }) {
+ for (const first of kAllWriteOps) {
+ for (const second of kAllWriteOps) {
+ if (checkOpsValidForContext([first, second], _context)) {
+ yield {
+ first: { op: first, in: _context[0] },
+ second: { op: second, in: _context[1] },
+ };
+ }
+ }
+ }
+ })
+ )
+ .fn(t => {
+ const helper = new TextureSyncTestHelper(t, {
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ kOpInfo[t.params.first.op].writeUsage |
+ kOpInfo[t.params.second.op].writeUsage,
+ });
+ // [2] Use non-solid-color texture value.
+ const texelValue1 = { R: 1, G: 0, B: 0, A: 1 } as const;
+ const texelValue2 = { R: 0, G: 1, B: 0, A: 1 } as const;
+
+ helper.performWriteOp(t.params.first, texelValue1);
+ helper.ensureBoundary(t.params.boundary);
+ helper.performWriteOp(t.params.second, texelValue2);
+ helper.ensureSubmit();
+
+ // Read back the contents so we can test the result.
+ const readbackTexture = helper.performReadOp({ op: 't2t-copy', in: 'command-encoder' });
+ helper.ensureSubmit();
+
+ // Contents should be the second value written.
+ t.expectSingleColor(readbackTexture, helper.kTextureFormat, {
+ size: [...helper.kTextureSize, 1],
+ exp: texelValue2,
+ });
+ });
+
+g.test('rw,single_pass,load_store')
+ .desc(
+ `
+ TODO: Test memory synchronization when loading from a texture subresource in a single pass and storing to it.`
+ )
+ .unimplemented();
+
+g.test('rw,single_pass,load_resolve')
+ .desc(
+ `
+ TODO: Test memory synchronization when loading from a texture subresource in a single pass and resolving to it.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/texture_sync_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/texture_sync_test.ts
new file mode 100644
index 0000000000..60a62098f4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/memory_sync/texture/texture_sync_test.ts
@@ -0,0 +1,124 @@
+import { GPUConst } from '../../../../constants.js';
+import { OperationContext } from '../operation_context_helper.js';
+
+export const kAllWriteOps = [
+ 'write-texture',
+ 'b2t-copy',
+ 't2t-copy',
+ 'storage',
+ 'attachment-store',
+ 'attachment-resolve',
+] as const;
+export type WriteOp = typeof kAllWriteOps[number];
+
+export const kAllReadOps = ['t2b-copy', 't2t-copy', 'sample'] as const;
+export type ReadOp = typeof kAllReadOps[number];
+
+export type Op = ReadOp | WriteOp;
+
+interface OpInfo {
+ readonly contexts: OperationContext[];
+ readonly readUsage: GPUTextureUsageFlags;
+ readonly writeUsage: GPUTextureUsageFlags;
+ // Add fields as needed
+}
+
+/**
+ * Mapping of Op to the OperationContext(s) it is valid in
+ */
+export const kOpInfo: {
+ readonly [k in Op]: OpInfo;
+} = /* prettier-ignore */ {
+ 'write-texture': {
+ contexts: [ 'queue' ],
+ readUsage: 0,
+ writeUsage: GPUConst.TextureUsage.COPY_DST,
+ },
+ 'b2t-copy': {
+ contexts: [ 'command-encoder' ],
+ readUsage: 0,
+ writeUsage: GPUConst.TextureUsage.COPY_DST,
+ },
+ 't2t-copy': {
+ contexts: [ 'command-encoder' ],
+ readUsage: GPUConst.TextureUsage.COPY_SRC,
+ writeUsage: GPUConst.TextureUsage.COPY_DST,
+ },
+ 't2b-copy': {
+ contexts: [ 'command-encoder' ],
+ readUsage: GPUConst.TextureUsage.COPY_SRC,
+ writeUsage: 0,
+ },
+ 'storage': {
+ contexts: [ 'compute-pass-encoder', 'render-pass-encoder', 'render-bundle-encoder' ],
+ readUsage: 0,
+ writeUsage: GPUConst.TextureUsage.STORAGE,
+ },
+ 'sample': {
+ contexts: [ 'compute-pass-encoder', 'render-pass-encoder', 'render-bundle-encoder' ],
+ readUsage: GPUConst.TextureUsage.SAMPLED,
+ writeUsage: 0,
+ },
+ 'attachment-store': {
+ contexts: [ 'command-encoder' ],
+ readUsage: 0,
+ writeUsage: GPUConst.TextureUsage.RENDER_ATTACHMENT,
+ },
+ 'attachment-resolve': {
+ contexts: [ 'command-encoder' ],
+ readUsage: 0,
+ writeUsage: GPUConst.TextureUsage.RENDER_ATTACHMENT,
+ },
+};
+
+export function checkOpsValidForContext(
+ ops: [Op, Op],
+ context: [OperationContext, OperationContext]
+) {
+ const valid =
+ kOpInfo[ops[0]].contexts.includes(context[0]) && kOpInfo[ops[1]].contexts.includes(context[1]);
+ if (!valid) return false;
+
+ if (
+ context[0] === 'render-bundle-encoder' ||
+ context[0] === 'render-pass-encoder' ||
+ context[1] === 'render-bundle-encoder' ||
+ context[1] === 'render-pass-encoder'
+ ) {
+ // In a render pass, it is invalid to use a resource as both writable and another usage.
+ // Also, for storage+storage usage, the application is opting into racy behavior.
+ // The storage+storage case is also skipped as the results cannot be reliably tested.
+ const checkImpl = (op1: Op, op2: Op) => {
+ switch (op1) {
+ case 'attachment-resolve':
+ case 'attachment-store':
+ case 'storage':
+ switch (op2) {
+ case 'attachment-resolve':
+ case 'attachment-store':
+ case 'storage':
+ case 'sample':
+ // Write+other, or racy.
+ return false;
+ case 'b2t-copy':
+ case 't2b-copy':
+ case 't2t-copy':
+ case 'write-texture':
+ // These don't occur in a render pass.
+ return true;
+ }
+ break;
+ case 'b2t-copy':
+ case 'sample':
+ case 't2b-copy':
+ case 't2t-copy':
+ case 'write-texture':
+ // These are not write usages, or don't occur in a render pass.
+ break;
+ }
+ return true;
+ };
+ return checkImpl(ops[0], ops[1]) && checkImpl(ops[1], ops[0]);
+ }
+ return true;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/onSubmittedWorkDone.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/onSubmittedWorkDone.spec.ts
new file mode 100644
index 0000000000..8b0647e7ef
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/onSubmittedWorkDone.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Tests for the behavior of GPUQueue.onSubmittedWorkDone().
+
+Note that any promise timeouts will be detected by the framework.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { range } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('without_work')
+ .desc(`Await onSubmittedWorkDone once without having submitted any work.`)
+ .fn(async t => {
+ await t.queue.onSubmittedWorkDone();
+ });
+
+g.test('with_work')
+ .desc(`Await onSubmittedWorkDone once after submitting some work (writeBuffer).`)
+ .fn(async t => {
+ const buffer = t.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST });
+ t.queue.writeBuffer(buffer, 0, new Uint8Array(4));
+ await t.queue.onSubmittedWorkDone();
+ });
+
+g.test('many,serial')
+ .desc(`Await 1000 onSubmittedWorkDone calls in serial.`)
+ .fn(async t => {
+ for (let i = 0; i < 1000; ++i) {
+ await t.queue.onSubmittedWorkDone();
+ }
+ });
+
+g.test('many,parallel')
+ .desc(`Await 1000 onSubmittedWorkDone calls in parallel with Promise.all().`)
+ .fn(async t => {
+ const promises = range(1000, () => t.queue.onSubmittedWorkDone());
+ await Promise.all(promises);
+ });
+
+g.test('many,parallel_order')
+ .desc(`Issue 200 onSubmittedWorkDone calls and make sure they resolve in the right order.`)
+ .fn(async t => {
+ const promises = [];
+ let lastResolved = -1;
+ for (const i of range(200, i => i)) {
+ promises.push(
+ t.queue.onSubmittedWorkDone().then(() => {
+ t.expect(i === lastResolved + 1);
+ lastResolved++;
+ })
+ );
+ }
+ await Promise.all(promises);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/pipeline/default_layout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/pipeline/default_layout.spec.ts
new file mode 100644
index 0000000000..f303b2737f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/pipeline/default_layout.spec.ts
@@ -0,0 +1,27 @@
+export const description = `
+Tests for default pipeline layouts.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('getBindGroupLayout_js_object')
+ .desc(
+ `Test that getBindGroupLayout returns [TODO: the same or a different, needs spec] object
+each time.`
+ )
+ .unimplemented();
+
+g.test('incompatible_with_explicit')
+ .desc(`Test that default bind group layouts are never compatible with explicitly created ones.`)
+ .unimplemented();
+
+g.test('layout')
+ .desc(
+ `Test that bind group layouts of the default pipeline layout are correct by passing various
+shaders and then checking their computed bind group layouts are compatible with particular bind
+groups.`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/queue/writeBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/queue/writeBuffer.spec.ts
new file mode 100644
index 0000000000..eb42d1a3c3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/queue/writeBuffer.spec.ts
@@ -0,0 +1,235 @@
+export const description = 'Operation tests for GPUQueue.writeBuffer()';
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { memcpy, range } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { align } from '../../../util/math.js';
+
+const kTypedArrays = [
+ 'Uint8Array',
+ 'Uint16Array',
+ 'Uint32Array',
+ 'Int8Array',
+ 'Int16Array',
+ 'Int32Array',
+ 'Float32Array',
+ 'Float64Array',
+] as const;
+
+type WriteBufferSignature = {
+ bufferOffset: number;
+ data: readonly number[];
+ arrayType: typeof kTypedArrays[number];
+ useArrayBuffer: boolean;
+ dataOffset?: number; // In elements when useArrayBuffer === false, bytes otherwise
+ dataSize?: number; // In elements when useArrayBuffer === false, bytes otherwise
+};
+
+class F extends GPUTest {
+ calculateRequiredBufferSize(writes: WriteBufferSignature[]): number {
+ let bufferSize = 0;
+ // Calculate size of final buffer
+ for (const { bufferOffset, data, arrayType, useArrayBuffer, dataOffset, dataSize } of writes) {
+ const TypedArrayConstructor = globalThis[arrayType];
+
+ // When passing data as an ArrayBuffer, dataOffset and dataSize use byte instead of number of
+ // elements. bytesPerElement is used to convert dataOffset and dataSize from elements to bytes
+ // when useArrayBuffer === false.
+ const bytesPerElement = useArrayBuffer ? 1 : TypedArrayConstructor.BYTES_PER_ELEMENT;
+
+ // Calculate the number of bytes written to the buffer. data is always an array of elements.
+ let bytesWritten =
+ data.length * TypedArrayConstructor.BYTES_PER_ELEMENT - (dataOffset || 0) * bytesPerElement;
+
+ if (dataSize) {
+ // When defined, dataSize clamps the number of bytes written
+ bytesWritten = Math.min(bytesWritten, dataSize * bytesPerElement);
+ }
+
+ // The minimum buffer size required for the write to succeed is the number of bytes written +
+ // the bufferOffset
+ const requiredBufferSize = bufferOffset + bytesWritten;
+
+ // Find the largest required size by all writes
+ bufferSize = Math.max(bufferSize, requiredBufferSize);
+ }
+ // writeBuffer requires buffers to be a multiple of 4
+ return align(bufferSize, 4);
+ }
+
+ testWriteBuffer(...writes: WriteBufferSignature[]) {
+ const bufferSize = this.calculateRequiredBufferSize(writes);
+
+ // Initialize buffer to non-zero data (0xff) for easier debug.
+ const expectedData = new Uint8Array(bufferSize).fill(0xff);
+
+ const buffer = this.makeBufferWithContents(
+ expectedData,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+
+ for (const { bufferOffset, data, arrayType, useArrayBuffer, dataOffset, dataSize } of writes) {
+ const TypedArrayConstructor = globalThis[arrayType];
+ const writeData = new TypedArrayConstructor(data);
+ const writeSrc = useArrayBuffer ? writeData.buffer : writeData;
+ this.queue.writeBuffer(buffer, bufferOffset, writeSrc, dataOffset, dataSize);
+ memcpy(
+ { src: writeSrc, start: dataOffset, length: dataSize },
+ { dst: expectedData, start: bufferOffset }
+ );
+ }
+
+ this.debug(`expectedData: [${expectedData.join(', ')}]`);
+ this.expectGPUBufferValuesEqual(buffer, expectedData);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kTestData = range<number>(16, i => i);
+
+g.test('array_types')
+ .desc('Tests that writeBuffer correctly handles different TypedArrays and ArrayBuffer.')
+ .params(u =>
+ u //
+ .combine('arrayType', kTypedArrays)
+ .combine('useArrayBuffer', [false, true])
+ )
+ .fn(t => {
+ const { arrayType, useArrayBuffer } = t.params;
+ const dataOffset = 1;
+ const dataSize = 8;
+ t.testWriteBuffer({
+ bufferOffset: 0,
+ arrayType,
+ data: kTestData,
+ dataOffset,
+ dataSize,
+ useArrayBuffer,
+ });
+ });
+
+g.test('multiple_writes_at_different_offsets_and_sizes')
+ .desc(
+ `
+Tests that writeBuffer currently handles different offsets and writes. This includes:
+- Non-overlapping TypedArrays and ArrayLists
+- Overlapping TypedArrays and ArrayLists
+- Writing zero data
+- Writing on zero sized buffers
+- Unaligned source
+- Multiple overlapping writes with decreasing sizes
+ `
+ )
+ .paramsSubcasesOnly([
+ {
+ // Concatenate 2 Uint32Arrays
+ writes: [
+ {
+ bufferOffset: 0,
+ data: kTestData,
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ dataOffset: 2,
+ dataSize: 2,
+ }, // [2, 3]
+ {
+ bufferOffset: 2 * Uint32Array.BYTES_PER_ELEMENT,
+ data: kTestData,
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ dataOffset: 0,
+ dataSize: 2,
+ }, // [0, 1]
+ ], // Expected [2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
+ },
+ {
+ // Concatenate 2 Uint8Arrays
+ writes: [
+ { bufferOffset: 0, data: [0, 1, 2, 3], arrayType: 'Uint8Array', useArrayBuffer: false },
+ { bufferOffset: 4, data: [4, 5, 6, 7], arrayType: 'Uint8Array', useArrayBuffer: false },
+ ], // Expected [0, 1, 2, 3, 4, 5, 6, 7]
+ },
+ {
+ // Overlap in the middle
+ writes: [
+ { bufferOffset: 0, data: kTestData, arrayType: 'Uint8Array', useArrayBuffer: false },
+ { bufferOffset: 4, data: [0], arrayType: 'Uint32Array', useArrayBuffer: false },
+ ], // Expected [0, 1, 2, 3, 0, 0 ,0 ,0, 8, 9, 10, 11, 12, 13, 14, 15]
+ },
+ {
+ // Overlapping arrayLists
+ writes: [
+ {
+ bufferOffset: 0,
+ data: kTestData,
+ arrayType: 'Uint32Array',
+ useArrayBuffer: true,
+ dataOffset: 2,
+ dataSize: 4 * Uint32Array.BYTES_PER_ELEMENT,
+ },
+ { bufferOffset: 4, data: [0x04030201], arrayType: 'Uint32Array', useArrayBuffer: true },
+ ], // Expected [0, 0, 1, 0, 1, 2, 3, 4, 0, 0, 3, 0, 0, 0, 4, 0]
+ },
+ {
+ // Write over with empty buffer
+ writes: [
+ { bufferOffset: 0, data: kTestData, arrayType: 'Uint8Array', useArrayBuffer: false },
+ { bufferOffset: 0, data: [], arrayType: 'Uint8Array', useArrayBuffer: false },
+ ], // Expected [0, 1, 2, 3, 4, 5 ,6 ,7, 8, 9, 10, 11, 12, 13, 14, 15]
+ },
+ {
+ // Zero buffer
+ writes: [{ bufferOffset: 0, data: [], arrayType: 'Uint8Array', useArrayBuffer: false }],
+ }, // Expected []
+ {
+ // Unaligned source
+ writes: [
+ {
+ bufferOffset: 0,
+ data: [0x77, ...kTestData],
+ arrayType: 'Uint8Array',
+ useArrayBuffer: false,
+ dataOffset: 1,
+ },
+ ], // Expected [0, 1, 2, 3, 4, 5 ,6 ,7, 8, 9, 10, 11, 12, 13, 14, 15]
+ },
+ {
+ // Multiple overlapping writes
+ writes: [
+ {
+ bufferOffset: 0,
+ data: [0x05050505, 0x05050505, 0x05050505, 0x05050505, 0x05050505],
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ },
+ {
+ bufferOffset: 0,
+ data: [0x04040404, 0x04040404, 0x04040404, 0x04040404],
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ },
+ {
+ bufferOffset: 0,
+ data: [0x03030303, 0x03030303, 0x03030303],
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ },
+ {
+ bufferOffset: 0,
+ data: [0x02020202, 0x02020202],
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ },
+ {
+ bufferOffset: 0,
+ data: [0x01010101],
+ arrayType: 'Uint32Array',
+ useArrayBuffer: false,
+ },
+ ], // Expected [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5]
+ },
+ ] as const)
+ .fn(t => {
+ t.testWriteBuffer(...t.params.writes);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts
new file mode 100644
index 0000000000..a1fc9cdc21
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/reflection.spec.ts
@@ -0,0 +1,137 @@
+export const description = `
+Tests that object attributes which reflect the object's creation properties are properly set.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { GPUConst } from '../../constants.js';
+import { GPUTest } from '../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('buffer_reflection_attributes')
+ .desc(`For every buffer attribute, the corresponding descriptor value is carried over.`)
+ .paramsSubcasesOnly(u =>
+ u.combine('descriptor', [
+ { size: 4, usage: GPUConst.BufferUsage.VERTEX },
+ {
+ size: 16,
+ usage:
+ GPUConst.BufferUsage.STORAGE |
+ GPUConst.BufferUsage.COPY_SRC |
+ GPUConst.BufferUsage.UNIFORM,
+ },
+ { size: 32, usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.COPY_DST },
+ {
+ size: 32,
+ usage: GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE,
+ invalid: true,
+ },
+ ] as const)
+ )
+ .fn(async t => {
+ const { descriptor } = t.params;
+
+ t.expectValidationError(() => {
+ const buffer = t.device.createBuffer(descriptor);
+
+ t.expect(buffer.size === descriptor.size);
+ t.expect(buffer.usage === descriptor.usage);
+ }, descriptor.invalid === true);
+ });
+
+g.test('texture_reflection_attributes')
+ .desc(`For every texture attribute, the corresponding descriptor value is carried over.`)
+ .paramsSubcasesOnly(u =>
+ u.combine('descriptor', [
+ {
+ size: { width: 4, height: 4 },
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.TEXTURE_BINDING,
+ },
+ {
+ size: { width: 8, height: 8, depthOrArrayLayers: 8 },
+ format: 'bgra8unorm',
+ usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.COPY_SRC,
+ },
+ {
+ size: [4, 4],
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.TEXTURE_BINDING,
+ mipLevelCount: 2,
+ },
+ {
+ size: [16, 16, 16],
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.TEXTURE_BINDING,
+ dimension: '3d',
+ },
+ {
+ size: [32],
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.TEXTURE_BINDING,
+ dimension: '1d',
+ },
+ {
+ size: { width: 4, height: 4 },
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.RENDER_ATTACHMENT,
+ sampleCount: 4,
+ },
+ {
+ size: { width: 4, height: 4 },
+ format: 'rgba8unorm',
+ usage: GPUConst.TextureUsage.TEXTURE_BINDING,
+ sampleCount: 4,
+ invalid: true,
+ },
+ ] as const)
+ )
+ .fn(async t => {
+ const { descriptor } = t.params;
+
+ let width: number;
+ let height: number;
+ let depthOrArrayLayers: number;
+ if (Array.isArray(descriptor.size)) {
+ width = descriptor.size[0];
+ height = descriptor.size[1] || 1;
+ depthOrArrayLayers = descriptor.size[2] || 1;
+ } else {
+ width = (descriptor.size as GPUExtent3DDict).width;
+ height = (descriptor.size as GPUExtent3DDict).height || 1;
+ depthOrArrayLayers = (descriptor.size as GPUExtent3DDict).depthOrArrayLayers || 1;
+ }
+
+ t.expectValidationError(() => {
+ const texture = t.device.createTexture(descriptor);
+
+ t.expect(texture.width === width);
+ t.expect(texture.height === height);
+ t.expect(texture.depthOrArrayLayers === depthOrArrayLayers);
+ t.expect(texture.format === descriptor.format);
+ t.expect(texture.usage === descriptor.usage);
+ t.expect(texture.dimension === (descriptor.dimension || '2d'));
+ t.expect(texture.mipLevelCount === (descriptor.mipLevelCount || 1));
+ t.expect(texture.sampleCount === (descriptor.sampleCount || 1));
+ }, descriptor.invalid === true);
+ });
+
+g.test('query_set_reflection_attributes')
+ .desc(`For every queue attribute, the corresponding descriptor value is carried over.`)
+ .paramsSubcasesOnly(u =>
+ u.combine('descriptor', [
+ { type: 'occlusion', count: 4 },
+ { type: 'occlusion', count: 16 },
+ { type: 'occlusion', count: 8193, invalid: true },
+ ] as const)
+ )
+ .fn(async t => {
+ const { descriptor } = t.params;
+
+ t.expectValidationError(() => {
+ const querySet = t.device.createQuerySet(descriptor);
+
+ t.expect(querySet.type === descriptor.type);
+ t.expect(querySet.count === descriptor.count);
+ }, descriptor.invalid === true);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/README.txt
new file mode 100644
index 0000000000..aedaaa2d83
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/README.txt
@@ -0,0 +1 @@
+Render pass stuff other than commands (which are in command_buffer/).
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/clear_value.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/clear_value.spec.ts
new file mode 100644
index 0000000000..9afa3f440d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/clear_value.spec.ts
@@ -0,0 +1,189 @@
+export const description = `
+Tests for render pass clear values.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ kDepthStencilFormats,
+ depthStencilFormatAspectSize,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stored')
+ .desc(`Test render pass clear values are stored at the end of an empty pass.`)
+ .unimplemented();
+
+g.test('loaded')
+ .desc(
+ `Test render pass clear values are visible during the pass by doing some trivial blending
+with the attachment (e.g. add [0,0,0,0] to the color and verify the stored result).`
+ )
+ .unimplemented();
+
+g.test('srgb')
+ .desc(
+ `Test that clear values on '-srgb' type attachments are interpreted as unencoded (linear),
+not decoded from srgb to linear.`
+ )
+ .unimplemented();
+
+g.test('layout')
+ .desc(
+ `Test that bind group layouts of the default pipeline layout are correct by passing various
+shaders and then checking their computed bind group layouts are compatible with particular bind
+groups.`
+ )
+ .unimplemented();
+
+g.test('stencil_clear_value')
+ .desc(
+ `Test that when stencilLoadOp is "clear", the stencil aspect should be correctly cleared by
+ GPURenderPassDepthStencilAttachment.stencilClearValue, which will be converted to the type of
+ the stencil aspect of view by taking the same number of LSBs as the number of bits in the
+ stencil aspect of one texel block of view.`
+ )
+ .params(u =>
+ u
+ .combine('stencilFormat', kDepthStencilFormats)
+ .combine('stencilClearValue', [0, 1, 0xff, 0x100 + 2, 0x10000 + 3])
+ .combine('applyStencilClearValueAsStencilReferenceValue', [true, false])
+ .filter(t => kTextureFormatInfo[t.stencilFormat].stencil)
+ )
+ .beforeAllSubcases(t => {
+ const { stencilFormat } = t.params;
+ const info = kTextureFormatInfo[stencilFormat];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ stencilFormat,
+ stencilClearValue,
+ applyStencilClearValueAsStencilReferenceValue,
+ } = t.params;
+
+ const kSize = [1, 1, 1] as const;
+ const colorFormat = 'rgba8unorm';
+ const stencilTexture = t.device.createTexture({
+ format: stencilFormat,
+ size: kSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ const colorTexture = t.device.createTexture({
+ format: colorFormat,
+ size: kSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ const renderPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorFormat }],
+ },
+ depthStencil: {
+ format: stencilFormat,
+ depthCompare: 'always',
+ stencilFront: {
+ compare: 'equal',
+ },
+ stencilBack: {
+ compare: 'equal',
+ },
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ const stencilAspectSizeInBytes = depthStencilFormatAspectSize(stencilFormat, 'stencil-only');
+ assert(stencilAspectSizeInBytes > 0);
+ const expectedStencilValue = stencilClearValue & ((stencilAspectSizeInBytes << 8) - 1);
+
+ // StencilReference used in setStencilReference will also be masked to the lowest valid bits, so
+ // no matter what we set in the rest high bits that will be masked out (different or same
+ // between stencilClearValue and stencilReference), the test will pass if and only if the valid
+ // lowest bits are the same.
+ const stencilReference = applyStencilClearValueAsStencilReferenceValue
+ ? stencilClearValue
+ : expectedStencilValue;
+
+ const encoder = t.device.createCommandEncoder();
+
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: stencilTexture.createView(),
+ stencilLoadOp: 'clear',
+ stencilStoreOp: 'store',
+ stencilClearValue,
+ };
+ if (kTextureFormatInfo[stencilFormat].depth) {
+ depthStencilAttachment.depthClearValue = 0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'store';
+ }
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ clearValue: [1, 0, 0, 1] as const,
+ },
+ ],
+ depthStencilAttachment,
+ });
+ renderPassEncoder.setPipeline(renderPipeline);
+ renderPassEncoder.setStencilReference(stencilReference);
+ renderPassEncoder.draw(6);
+ renderPassEncoder.end();
+
+ const destinationBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ size: 4,
+ });
+ t.trackForCleanup(destinationBuffer);
+ encoder.copyTextureToBuffer(
+ {
+ texture: stencilTexture,
+ aspect: 'stencil-only',
+ },
+ {
+ buffer: destinationBuffer,
+ },
+ [1, 1, 1]
+ );
+
+ t.queue.submit([encoder.finish()]);
+
+ t.expectSingleColor(colorTexture, colorFormat, {
+ size: [1, 1, 1],
+ exp: { R: 0, G: 1, B: 0, A: 1 },
+ });
+ t.expectGPUBufferValuesEqual(destinationBuffer, new Uint8Array([expectedStencilValue]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/resolve.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/resolve.spec.ts
new file mode 100644
index 0000000000..169817982a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/resolve.spec.ts
@@ -0,0 +1,205 @@
+export const description = `API Operation Tests for RenderPass StoreOp.
+Tests a render pass with a resolveTarget resolves correctly for many combinations of:
+ - number of color attachments, some with and some without a resolveTarget
+ - renderPass storeOp set to {'store', 'discard'}
+ - resolveTarget mip level {0, >0} (TODO?: different mip level from colorAttachment)
+ - resolveTarget {2d array layer, TODO: 3d slice} {0, >0} with {2d, TODO: 3d} resolveTarget
+ (TODO?: different z from colorAttachment)
+ - TODO: test all renderable color formats
+ - TODO: test that any not-resolved attachments are rendered to correctly.
+ - TODO: test different loadOps
+ - TODO?: resolveTarget mip level {0, >0} (TODO?: different mip level from colorAttachment)
+ - TODO?: resolveTarget {2d array layer, TODO: 3d slice} {0, >0} with {2d, TODO: 3d} resolveTarget
+ (different z from colorAttachment)
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+const kSlotsToResolve = [
+ [0, 2],
+ [1, 3],
+ [0, 1, 2, 3],
+];
+
+const kSize = 4;
+const kFormat: GPUTextureFormat = 'rgba8unorm';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('render_pass_resolve')
+ .params(u =>
+ u
+ .combine('storeOperation', ['discard', 'store'] as const)
+ .beginSubcases()
+ .combine('numColorAttachments', [2, 4] as const)
+ .combine('slotsToResolve', kSlotsToResolve)
+ .combine('resolveTargetBaseMipLevel', [0, 1] as const)
+ .combine('resolveTargetBaseArrayLayer', [0, 1] as const)
+ )
+ .fn(t => {
+ const targets: GPUColorTargetState[] = [];
+ for (let i = 0; i < t.params.numColorAttachments; i++) {
+ targets.push({ format: kFormat });
+ }
+
+ // These shaders will draw a white triangle into a texture. After draw, the top left
+ // half of the texture will be white, and the bottom right half will be unchanged. When this
+ // texture is resolved, there will be two distinct colors in each portion of the texture, as
+ // well as a line between the portions that contain the midpoint color due to the multisample
+ // resolve.
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Output {
+ @location(0) fragColor0 : vec4<f32>,
+ @location(1) fragColor1 : vec4<f32>,
+ @location(2) fragColor2 : vec4<f32>,
+ @location(3) fragColor3 : vec4<f32>,
+ };
+
+ @fragment fn main() -> Output {
+ return Output(
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
+ );
+ }`,
+ }),
+ entryPoint: 'main',
+ targets,
+ },
+ primitive: { topology: 'triangle-list' },
+ multisample: { count: 4 },
+ });
+
+ const resolveTargets: GPUTexture[] = [];
+ const renderPassColorAttachments: GPURenderPassColorAttachment[] = [];
+
+ // The resolve target must be the same size as the color attachment. If we're resolving to mip
+ // level 1, the resolve target base mip level should be 2x the color attachment size.
+ const kResolveTargetSize = kSize << t.params.resolveTargetBaseMipLevel;
+
+ for (let i = 0; i < t.params.numColorAttachments; i++) {
+ const colorAttachment = t.device.createTexture({
+ format: kFormat,
+ size: { width: kSize, height: kSize, depthOrArrayLayers: 1 },
+ sampleCount: 4,
+ mipLevelCount: 1,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ if (t.params.slotsToResolve.includes(i)) {
+ const colorAttachment = t.device.createTexture({
+ format: kFormat,
+ size: { width: kSize, height: kSize, depthOrArrayLayers: 1 },
+ sampleCount: 4,
+ mipLevelCount: 1,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const resolveTarget = t.device.createTexture({
+ format: kFormat,
+ size: {
+ width: kResolveTargetSize,
+ height: kResolveTargetSize,
+ depthOrArrayLayers: t.params.resolveTargetBaseArrayLayer + 1,
+ },
+ sampleCount: 1,
+ mipLevelCount: t.params.resolveTargetBaseMipLevel + 1,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ // Clear to black for the load operation. After the draw, the top left half of the attachment
+ // will be white and the bottom right half will be black.
+ renderPassColorAttachments.push({
+ view: colorAttachment.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: t.params.storeOperation,
+ resolveTarget: resolveTarget.createView({
+ baseMipLevel: t.params.resolveTargetBaseMipLevel,
+ baseArrayLayer: t.params.resolveTargetBaseArrayLayer,
+ }),
+ });
+
+ resolveTargets.push(resolveTarget);
+ } else {
+ renderPassColorAttachments.push({
+ view: colorAttachment.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: t.params.storeOperation,
+ });
+ }
+ }
+
+ const encoder = t.device.createCommandEncoder();
+
+ const pass = encoder.beginRenderPass({
+ colorAttachments: renderPassColorAttachments,
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // Verify the resolve targets contain the correct values.
+ for (const resolveTarget of resolveTargets) {
+ // Test top left pixel, which should be {255, 255, 255, 255}.
+ t.expectSinglePixelIn2DTexture(
+ resolveTarget,
+ kFormat,
+ { x: 0, y: 0 },
+ {
+ exp: new Uint8Array([0xff, 0xff, 0xff, 0xff]),
+ slice: t.params.resolveTargetBaseArrayLayer,
+ layout: { mipLevel: t.params.resolveTargetBaseMipLevel },
+ }
+ );
+
+ // Test bottom right pixel, which should be {0, 0, 0, 0}.
+ t.expectSinglePixelIn2DTexture(
+ resolveTarget,
+ kFormat,
+ { x: kSize - 1, y: kSize - 1 },
+ {
+ exp: new Uint8Array([0x00, 0x00, 0x00, 0x00]),
+ slice: t.params.resolveTargetBaseArrayLayer,
+ layout: { mipLevel: t.params.resolveTargetBaseMipLevel },
+ }
+ );
+
+ // Test top right pixel, which should be {127, 127, 127, 127} due to the multisampled resolve.
+ t.expectSinglePixelBetweenTwoValuesIn2DTexture(
+ resolveTarget,
+ kFormat,
+ { x: kSize - 1, y: 0 },
+ {
+ exp: [new Uint8Array([0x7f, 0x7f, 0x7f, 0x7f]), new Uint8Array([0x80, 0x80, 0x80, 0x80])],
+ slice: t.params.resolveTargetBaseArrayLayer,
+ layout: { mipLevel: t.params.resolveTargetBaseMipLevel },
+ }
+ );
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeOp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeOp.spec.ts
new file mode 100644
index 0000000000..75fa927728
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeOp.spec.ts
@@ -0,0 +1,354 @@
+export const description = `API Operation Tests for RenderPass StoreOp.
+
+ Test Coverage:
+
+ - Tests that color and depth-stencil store operations {'discard', 'store'} work correctly for a
+ render pass with both a color attachment and depth-stencil attachment.
+ TODO: use depth24plus-stencil8
+
+ - Tests that store operations {'discard', 'store'} work correctly for a render pass with multiple
+ color attachments.
+ TODO: test with more interesting loadOp values
+
+ - Tests that store operations {'discard', 'store'} work correctly for a render pass with a color
+ attachment for:
+ - All renderable color formats
+ - mip level set to {'0', mip > '0'}
+ - array layer set to {'0', layer > '1'} for 2D textures
+ TODO: depth slice set to {'0', slice > '0'} for 3D textures
+
+ - Tests that store operations {'discard', 'store'} work correctly for a render pass with a
+ depth-stencil attachment for:
+ - All renderable depth-stencil formats
+ - mip level set to {'0', mip > '0'}
+ - array layer set to {'0', layer > '1'} for 2D textures
+ TODO: test depth24plus and depth24plus-stencil8 formats
+ TODO: test that depth and stencil aspects are set separately
+ TODO: depth slice set to {'0', slice > '0'} for 3D textures
+ TODO: test with more interesting loadOp values`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ kTextureFormatInfo,
+ kEncodableTextureFormats,
+ kSizedDepthStencilFormats,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { PerTexelComponent } from '../../../util/texture/texel_data.js';
+
+// Test with a zero and non-zero mip.
+const kMipLevel: number[] = [0, 1];
+const kMipLevelCount = 2;
+
+// Test with different numbers of color attachments.
+type NumColorAttachments = 1 | 2 | 3 | 4;
+const kNumColorAttachments: NumColorAttachments[] = [1, 2, 3, 4];
+
+// Test with a zero and non-zero array layer.
+const kArrayLayers: number[] = [0, 1];
+
+const kStoreOps: GPUStoreOp[] = ['discard', 'store'];
+
+const kHeight = 2;
+const kWidth = 2;
+
+export const g = makeTestGroup(GPUTest);
+
+// Tests a render pass with both a color and depth stencil attachment to ensure store operations are
+// set independently.
+g.test('render_pass_store_op,color_attachment_with_depth_stencil_attachment')
+ .params(u =>
+ u //
+ .combine('colorStoreOperation', kStoreOps)
+ .combine('depthStencilStoreOperation', kStoreOps)
+ )
+ .fn(t => {
+ // Create a basic color attachment.
+ const kColorFormat: GPUTextureFormat = 'rgba8unorm';
+ const colorAttachment = t.device.createTexture({
+ format: kColorFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const colorAttachmentView = colorAttachment.createView();
+
+ // Create a basic depth/stencil attachment.
+ const kDepthStencilFormat: GPUTextureFormat = 'depth32float';
+ const depthStencilAttachment = t.device.createTexture({
+ format: kDepthStencilFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ // Color load operation will clear to {1.0, 1.0, 1.0, 1.0}.
+ // Depth operation will clear to 1.0.
+ // Store operations are determined by test the params.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: t.params.colorStoreOperation,
+ },
+ ],
+ depthStencilAttachment: {
+ view: depthStencilAttachment.createView(),
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: t.params.depthStencilStoreOperation,
+ },
+ });
+ pass.end();
+
+ t.device.queue.submit([encoder.finish()]);
+
+ // Check that the correct store operation occurred.
+ let expectedColorValue: PerTexelComponent<number> = {};
+ if (t.params.colorStoreOperation === 'discard') {
+ // If colorStoreOp was clear, the texture should now contain {0.0, 0.0, 0.0, 0.0}.
+ expectedColorValue = { R: 0.0, G: 0.0, B: 0.0, A: 0.0 };
+ } else if (t.params.colorStoreOperation === 'store') {
+ // If colorStoreOP was store, the texture should still contain {1.0, 1.0, 1.0, 1.0}.
+ expectedColorValue = { R: 1.0, G: 1.0, B: 1.0, A: 1.0 };
+ }
+ t.expectSingleColor(colorAttachment, kColorFormat, {
+ size: [kHeight, kWidth, 1],
+ exp: expectedColorValue,
+ });
+
+ // Check that the correct store operation occurred.
+ let expectedDepthValue: PerTexelComponent<number> = {};
+ if (t.params.depthStencilStoreOperation === 'discard') {
+ // If depthStencilStoreOperation was clear, the texture's depth component should be 0.0, and
+ // the stencil component should be 0.0.
+ expectedDepthValue = { Depth: 0.0 };
+ } else if (t.params.depthStencilStoreOperation === 'store') {
+ // If depthStencilStoreOperation was store, the texture's depth component should be 1.0, and
+ // the stencil component should be 1.0.
+ expectedDepthValue = { Depth: 1.0 };
+ }
+ t.expectSingleColor(depthStencilAttachment, kDepthStencilFormat, {
+ size: [kHeight, kWidth, 1],
+ exp: expectedDepthValue,
+ layout: { mipLevel: 0, aspect: 'depth-only' },
+ });
+ });
+
+// Tests that render pass color attachment store operations work correctly for all renderable color
+// formats, mip levels and array layers.
+g.test('render_pass_store_op,color_attachment_only')
+ .params(u =>
+ u
+ .combine('colorFormat', kEncodableTextureFormats)
+ // Filter out any non-renderable formats
+ .filter(({ colorFormat }) => {
+ const info = kTextureFormatInfo[colorFormat];
+ return info.color && info.renderable;
+ })
+ .combine('storeOperation', kStoreOps)
+ .beginSubcases()
+ .combine('mipLevel', kMipLevel)
+ .combine('arrayLayer', kArrayLayers)
+ )
+ .fn(t => {
+ const colorAttachment = t.device.createTexture({
+ format: t.params.colorFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: t.params.arrayLayer + 1 },
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const colorViewDesc: GPUTextureViewDescriptor = {
+ baseArrayLayer: t.params.arrayLayer,
+ baseMipLevel: t.params.mipLevel,
+ mipLevelCount: 1,
+ arrayLayerCount: 1,
+ };
+
+ const colorAttachmentView = colorAttachment.createView(colorViewDesc);
+
+ // Color load operation will clear to {1.0, 0.0, 0.0, 1.0}.
+ // Color store operation is determined by the test params.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: t.params.storeOperation,
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // Check that the correct store operation occurred.
+ let expectedValue: PerTexelComponent<number> = {};
+ if (t.params.storeOperation === 'discard') {
+ // If colorStoreOp was clear, the texture should now contain {0.0, 0.0, 0.0, 0.0}.
+ expectedValue = { R: 0.0, G: 0.0, B: 0.0, A: 0.0 };
+ } else if (t.params.storeOperation === 'store') {
+ // If colorStoreOP was store, the texture should still contain {1.0, 0.0, 0.0, 1.0}.
+ expectedValue = { R: 1.0, G: 0.0, B: 0.0, A: 1.0 };
+ }
+
+ t.expectSingleColor(colorAttachment, t.params.colorFormat, {
+ size: [kHeight, kWidth, 1],
+ slice: t.params.arrayLayer,
+ exp: expectedValue,
+ layout: { mipLevel: t.params.mipLevel },
+ });
+ });
+
+// Test with multiple color attachments to ensure each attachment's storeOp is set independently.
+g.test('render_pass_store_op,multiple_color_attachments')
+ .params(u =>
+ u
+ .combine('storeOperation1', kStoreOps)
+ .combine('storeOperation2', kStoreOps)
+ .beginSubcases()
+ .combine('colorAttachments', kNumColorAttachments)
+ )
+ .fn(t => {
+ const kColorFormat: GPUTextureFormat = 'rgba8unorm';
+ const colorAttachments: GPUTexture[] = [];
+
+ for (let i = 0; i < t.params.colorAttachments; i++) {
+ colorAttachments.push(
+ t.device.createTexture({
+ format: kColorFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ );
+ }
+
+ // Color load operation will clear to {1.0, 1.0, 1.0, 1.0}
+ // Color store operation is determined by test params. Use storeOperation1 for even numbered
+ // attachments and storeOperation2 for odd numbered attachments.
+ const renderPassColorAttachments: GPURenderPassColorAttachment[] = [];
+ for (let i = 0; i < t.params.colorAttachments; i++) {
+ renderPassColorAttachments.push({
+ view: colorAttachments[i].createView(),
+ clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: i % 2 === 0 ? t.params.storeOperation1 : t.params.storeOperation2,
+ });
+ }
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: renderPassColorAttachments,
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // Check that the correct store operation occurred.
+ let expectedValue: PerTexelComponent<number> = {};
+ for (let i = 0; i < t.params.colorAttachments; i++) {
+ if (renderPassColorAttachments[i].storeOp === 'discard') {
+ // If colorStoreOp was clear, the texture should now contain {0.0, 0.0, 0.0, 0.0}.
+ expectedValue = { R: 0.0, G: 0.0, B: 0.0, A: 0.0 };
+ } else if (renderPassColorAttachments[i].storeOp === 'store') {
+ // If colorStoreOP was store, the texture should still contain {1.0, 1.0, 1.0, 1.0}.
+ expectedValue = { R: 1.0, G: 1.0, B: 1.0, A: 1.0 };
+ }
+ t.expectSingleColor(colorAttachments[i], kColorFormat, {
+ size: [kHeight, kWidth, 1],
+ exp: expectedValue,
+ });
+ }
+ });
+
+g.test('render_pass_store_op,depth_stencil_attachment_only')
+ .desc(
+ `
+Tests that render pass depth stencil store operations work correctly for all renderable color
+formats, mip levels and array layers.
+
+- x= all (sized) depth stencil formats, all store ops, multiple mip levels, multiple array layers
+
+TODO: Also test unsized depth/stencil formats [1]
+ `
+ )
+ .params(u =>
+ u
+ .combine('depthStencilFormat', kSizedDepthStencilFormats) // [1]
+ .combine('storeOperation', kStoreOps)
+ .beginSubcases()
+ .combine('mipLevel', kMipLevel)
+ .combine('arrayLayer', kArrayLayers)
+ )
+ .fn(t => {
+ const depthStencilTexture = t.device.createTexture({
+ format: t.params.depthStencilFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: t.params.arrayLayer + 1 },
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const depthStencilViewDesc: GPUTextureViewDescriptor = {
+ baseArrayLayer: t.params.arrayLayer,
+ baseMipLevel: t.params.mipLevel,
+ mipLevelCount: 1,
+ arrayLayerCount: 1,
+ };
+
+ const depthStencilAttachmentView = depthStencilTexture.createView(depthStencilViewDesc);
+
+ // Depth-stencil load operation will clear to depth = 1.0, stencil = 1.0.
+ // Depth-stencil store operate is determined by test params.
+ const encoder = t.device.createCommandEncoder();
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthStencilAttachmentView,
+ };
+ if (kTextureFormatInfo[t.params.depthStencilFormat].depth) {
+ depthStencilAttachment.depthClearValue = 1.0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = t.params.storeOperation;
+ }
+ if (kTextureFormatInfo[t.params.depthStencilFormat].stencil) {
+ depthStencilAttachment.stencilClearValue = 1;
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = t.params.storeOperation;
+ }
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment,
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ let expectedDepthValue: PerTexelComponent<number> = {};
+ let expectedStencilValue: PerTexelComponent<number> = {};
+ if (t.params.storeOperation === 'discard') {
+ // If depthStencilStoreOperation was clear, the texture's depth/stencil component should be 0,
+ expectedDepthValue = { Depth: 0.0 };
+ expectedStencilValue = { Stencil: 0 };
+ } else if (t.params.storeOperation === 'store') {
+ // If depthStencilStoreOperation was store, the texture's depth/stencil components should be 1,
+ expectedDepthValue = { Depth: 1.0 };
+ expectedStencilValue = { Stencil: 1 };
+ }
+
+ if (kTextureFormatInfo[t.params.depthStencilFormat].depth) {
+ t.expectSingleColor(depthStencilTexture, t.params.depthStencilFormat, {
+ size: [kHeight, kWidth, 1],
+ slice: t.params.arrayLayer,
+ exp: expectedDepthValue,
+ layout: { mipLevel: t.params.mipLevel, aspect: 'depth-only' },
+ });
+ }
+ if (kTextureFormatInfo[t.params.depthStencilFormat].stencil) {
+ t.expectSingleColor(depthStencilTexture, t.params.depthStencilFormat, {
+ size: [kHeight, kWidth, 1],
+ slice: t.params.arrayLayer,
+ exp: expectedStencilValue,
+ layout: { mipLevel: t.params.mipLevel, aspect: 'stencil-only' },
+ });
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeop2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeop2.spec.ts
new file mode 100644
index 0000000000..1a670448af
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pass/storeop2.spec.ts
@@ -0,0 +1,83 @@
+export const description = `
+renderPass store op test that drawn quad is either stored or cleared based on storeop
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('storeOp_controls_whether_1x1_drawn_quad_is_stored')
+ .desc(
+ `
+TODO: is this duplicated with api,operation,render_pass,storeOp?
+TODO: needs review and rename
+`
+ )
+ .paramsSimple([
+ { storeOp: 'store', _expected: 1 }, //
+ { storeOp: 'discard', _expected: 0 },
+ ] as const)
+ .fn(async t => {
+ const renderTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'r8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ // create render pipeline
+ const renderPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'r8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ // encode pass and submit
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTexture.createView(),
+ storeOp: t.params.storeOp,
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(renderPipeline);
+ pass.draw(3);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // expect the buffer to be clear
+ t.expectSingleColor(renderTexture, 'r8unorm', {
+ size: [1, 1, 1],
+ exp: { R: t.params._expected },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/alpha_to_coverage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/alpha_to_coverage.spec.ts
new file mode 100644
index 0000000000..0b234759ef
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/alpha_to_coverage.spec.ts
@@ -0,0 +1,19 @@
+export const description = `
+TODO:
+- for sampleCount = 4, alphaToCoverageEnabled = true and various combinations of:
+ - rasterization masks
+ - increasing alpha values of the first color output including { < 0, = 0, = 1/16, = 2/16, ..., = 15/16, = 1, > 1 }
+ - alpha values of the second color output = { 0, 0.5, 1.0 }.
+- test that for a single pixel in { first, second } { color, depth, stencil } output the final sample mask is applied to it, moreover:
+ - if alpha is 0.0 or less then alpha to coverage mask is 0x0,
+ - if alpha is 1.0 or greater then alpha to coverage mask is 0xFFFFFFFF,
+ - that the number of bits in the alpha to coverage mask is non-decreasing,
+ - that the computation of alpha to coverage mask doesn't depend on any other color output than first,
+ - (not included in the spec): that once a sample is included in the alpha to coverage sample mask
+ it will be included for any alpha greater than or equal to the current value.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/culling_tests.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/culling_tests.spec.ts
new file mode 100644
index 0000000000..521cf7b208
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/culling_tests.spec.ts
@@ -0,0 +1,185 @@
+export const description = `Test culling and rasterization state.
+
+Test coverage:
+Test all culling combinations of GPUFrontFace and GPUCullMode show the correct output.
+
+Use 2 triangles with different winding orders:
+
+- Test that the counter-clock wise triangle has correct output for:
+ - All FrontFaces (ccw, cw)
+ - All CullModes (none, front, back)
+ - All depth stencil attachment types (none, depth24plus, depth32float, depth24plus-stencil8)
+ - Some primitive topologies (triangle-list, TODO: triangle-strip)
+
+- Test that the clock wise triangle has correct output for:
+ - All FrontFaces (ccw, cw)
+ - All CullModes (none, front, back)
+ - All depth stencil attachment types (none, depth24plus, depth32float, depth24plus-stencil8)
+ - Some primitive topologies (triangle-list, TODO: triangle-strip)
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kTextureFormatInfo } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+function faceIsCulled(face: 'cw' | 'ccw', frontFace: GPUFrontFace, cullMode: GPUCullMode): boolean {
+ return cullMode !== 'none' && (frontFace === face) === (cullMode === 'front');
+}
+
+function faceColor(face: 'cw' | 'ccw', frontFace: GPUFrontFace, cullMode: GPUCullMode): Uint8Array {
+ // front facing color is green, non front facing is red, background is blue
+ const isCulled = faceIsCulled(face, frontFace, cullMode);
+ if (!isCulled && face === frontFace) {
+ return new Uint8Array([0x00, 0xff, 0x00, 0xff]);
+ } else if (isCulled) {
+ return new Uint8Array([0x00, 0x00, 0xff, 0xff]);
+ } else {
+ return new Uint8Array([0xff, 0x00, 0x00, 0xff]);
+ }
+}
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('culling')
+ .desc(
+ `
+TODO: test triangle-strip as well [1]
+TODO: check the contents of the depth and stencil outputs [2]
+`
+ )
+ .params(
+ u =>
+ u
+ .combine('frontFace', ['ccw', 'cw'] as const)
+ .combine('cullMode', ['none', 'front', 'back'] as const)
+ .beginSubcases()
+ .combine('depthStencilFormat', [
+ null,
+ 'depth24plus',
+ 'depth32float',
+ 'depth24plus-stencil8',
+ ] as const)
+ .combine('primitiveTopology', ['triangle-list'] as const) // [1]
+ )
+ .fn(t => {
+ const size = 4;
+ const format = 'rgba8unorm';
+
+ const texture = t.device.createTexture({
+ size: { width: size, height: size, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+
+ let depthTexture: GPUTexture | undefined = undefined;
+ let depthStencilAttachment: GPURenderPassDepthStencilAttachment | undefined = undefined;
+ if (t.params.depthStencilFormat) {
+ depthTexture = t.device.createTexture({
+ size: { width: size, height: size, depthOrArrayLayers: 1 },
+ format: t.params.depthStencilFormat,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ depthStencilAttachment = {
+ view: depthTexture.createView(),
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ };
+
+ if (t.params.depthStencilFormat && kTextureFormatInfo[t.params.depthStencilFormat].stencil) {
+ depthStencilAttachment.stencilClearValue = 0;
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+ }
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 1.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment,
+ });
+
+ // Draw two triangles with different winding orders:
+ // 1. The top-left one is counterclockwise (CCW)
+ // 2. The bottom-right one is clockwise (CW)
+ pass.setPipeline(
+ t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, 0.0),
+ vec2<f32>( 0.0, 1.0),
+ vec2<f32>( 0.0, -1.0),
+ vec2<f32>( 1.0, 0.0),
+ vec2<f32>( 1.0, -1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main(
+ @builtin(front_facing) FrontFacing : bool
+ ) -> @location(0) vec4<f32> {
+ var color : vec4<f32>;
+ if (FrontFacing) {
+ color = vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ } else {
+ color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ return color;
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: {
+ topology: t.params.primitiveTopology,
+ frontFace: t.params.frontFace,
+ cullMode: t.params.cullMode,
+ },
+ depthStencil: depthTexture
+ ? { format: t.params.depthStencilFormat as GPUTextureFormat }
+ : undefined,
+ })
+ );
+
+ pass.draw(6, 1, 0, 0);
+ pass.end();
+
+ t.device.queue.submit([encoder.finish()]);
+
+ // front facing color is green, non front facing is red, background is blue
+ const kCCWTriangleTopLeftColor = faceColor('ccw', t.params.frontFace, t.params.cullMode);
+ t.expectSinglePixelIn2DTexture(
+ texture,
+ format,
+ { x: 0, y: 0 },
+ { exp: kCCWTriangleTopLeftColor }
+ );
+
+ const kCWTriangleBottomRightColor = faceColor('cw', t.params.frontFace, t.params.cullMode);
+ t.expectSinglePixelIn2DTexture(
+ texture,
+ format,
+ { x: size - 1, y: size - 1 },
+ { exp: kCWTriangleBottomRightColor }
+ );
+ // [2]: check the contents of the depth and stencil outputs
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/overrides.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/overrides.spec.ts
new file mode 100644
index 0000000000..598d75636d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/overrides.spec.ts
@@ -0,0 +1,456 @@
+export const description = `
+Testing render pipeline using overridable constants in vertex stage and fragment stage.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { PerTexelComponent } from '../../../util/texture/texel_data.js';
+
+class F extends GPUTest {
+ async ExpectShaderOutputWithConstants(
+ isAsync: boolean,
+ format: GPUTextureFormat,
+ expected: PerTexelComponent<number>,
+ vertex: GPUVertexState,
+ fragment: GPUFragmentState
+ ) {
+ const renderTarget = this.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const descriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex,
+ fragment,
+ primitive: {
+ topology: 'triangle-list',
+ frontFace: 'ccw',
+ cullMode: 'back',
+ },
+ };
+
+ const promise = isAsync
+ ? this.device.createRenderPipelineAsync(descriptor)
+ : Promise.resolve(this.device.createRenderPipeline(descriptor));
+
+ const pipeline = await promise;
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ clearValue: {
+ r: kClearValueResult.R,
+ g: kClearValueResult.G,
+ b: kClearValueResult.B,
+ a: kClearValueResult.A,
+ },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ this.expectSingleColor(renderTarget, format, {
+ size: [1, 1, 1],
+ exp: expected,
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kClearValueResult = { R: 0.2, G: 0.4, B: 0.6, A: 0.8 };
+const kDefaultValueResult = { R: 1.0, G: 1.0, B: 1.0, A: 1.0 };
+
+const kFullScreenTriangleVertexShader = `
+override xright: f32 = 3.0;
+override ytop: f32 = 3.0;
+
+@vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, ytop),
+ vec2<f32>(-1.0, -ytop),
+ vec2<f32>(xright, 0.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+}
+`;
+
+const kFullScreenTriangleFragmentShader = `
+override R: f32 = 1.0;
+override G: f32 = 1.0;
+override B: f32 = 1.0;
+override A: f32 = 1.0;
+
+@fragment fn main()
+ -> @location(0) vec4<f32> {
+ return vec4<f32>(R, G, B, A);
+}
+`;
+
+g.test('basic')
+ .desc(
+ `Test that either correct constants override values or default values when no constants override value are provided at pipeline creation time are used correctly in vertex and fragment shader.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [true, false])
+ .beginSubcases()
+ .combineWithParams([
+ {
+ expected: kDefaultValueResult,
+ vertexConstants: {},
+ fragmentConstants: {},
+ },
+ {
+ expected: kClearValueResult,
+ vertexConstants: {
+ xright: -3.0,
+ } as Record<string, GPUPipelineConstantValue>,
+ fragmentConstants: {},
+ },
+ {
+ expected: kClearValueResult,
+ vertexConstants: {
+ ytop: -3.0,
+ } as Record<string, GPUPipelineConstantValue>,
+ fragmentConstants: {},
+ },
+ {
+ expected: kDefaultValueResult,
+ vertexConstants: {
+ xright: 4.0,
+ ytop: 4.0,
+ } as Record<string, GPUPipelineConstantValue>,
+ fragmentConstants: {},
+ },
+ {
+ expected: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ vertexConstants: {},
+ fragmentConstants: { R: 0.0, B: 0.0 } as Record<string, GPUPipelineConstantValue>,
+ },
+ {
+ expected: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 },
+ vertexConstants: {},
+ fragmentConstants: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ ])
+ )
+ .fn(async t => {
+ const format = 'bgra8unorm';
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ format,
+ t.params.expected,
+ {
+ module: t.device.createShaderModule({
+ code: kFullScreenTriangleVertexShader,
+ }),
+ entryPoint: 'main',
+ constants: t.params.vertexConstants,
+ },
+ {
+ module: t.device.createShaderModule({
+ code: kFullScreenTriangleFragmentShader,
+ }),
+ entryPoint: 'main',
+ constants: t.params.fragmentConstants,
+ targets: [{ format }],
+ }
+ );
+ });
+
+g.test('precision')
+ .desc(`Test that the float number precision is preserved for constants`)
+ .params(u =>
+ u
+ .combine('isAsync', [true, false])
+ .beginSubcases()
+ .combineWithParams([
+ {
+ expected: { R: 3.14159, G: 1.0, B: 1.0, A: 1.0 },
+ vertexConstants: {},
+ fragmentConstants: { R: 3.14159 } as Record<string, GPUPipelineConstantValue>,
+ },
+ {
+ expected: { R: 3.141592653589793238, G: 1.0, B: 1.0, A: 1.0 },
+ vertexConstants: {},
+ fragmentConstants: { R: 3.141592653589793238 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ ])
+ )
+ .fn(async t => {
+ const format = 'rgba32float';
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ format,
+ t.params.expected,
+ {
+ module: t.device.createShaderModule({
+ code: kFullScreenTriangleVertexShader,
+ }),
+ entryPoint: 'main',
+ constants: t.params.vertexConstants,
+ },
+ {
+ module: t.device.createShaderModule({
+ code: kFullScreenTriangleFragmentShader,
+ }),
+ entryPoint: 'main',
+ constants: t.params.fragmentConstants,
+ targets: [{ format }],
+ }
+ );
+ });
+
+g.test('shared_shader_module')
+ .desc(
+ `Test that when the same module is shared by different pipelines, the constant values are still being used correctly.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [true, false])
+ .beginSubcases()
+ .combineWithParams([
+ {
+ expected0: kClearValueResult,
+ vertexConstants0: {
+ xright: -3.0,
+ } as Record<string, GPUPipelineConstantValue>,
+ fragmentConstants0: {},
+
+ expected1: kDefaultValueResult,
+ vertexConstants1: {},
+ fragmentConstants1: {},
+ },
+ {
+ expected0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 },
+ vertexConstants0: {},
+ fragmentConstants0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+
+ expected1: kDefaultValueResult,
+ vertexConstants1: {},
+ fragmentConstants1: {},
+ },
+ {
+ expected0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 },
+ vertexConstants0: {},
+ fragmentConstants0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+
+ expected1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ vertexConstants1: {},
+ fragmentConstants1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ ])
+ )
+ .fn(async t => {
+ const format = 'bgra8unorm';
+ const vertexModule = t.device.createShaderModule({
+ code: kFullScreenTriangleVertexShader,
+ });
+
+ const fragmentModule = t.device.createShaderModule({
+ code: kFullScreenTriangleFragmentShader,
+ });
+
+ const createPipelineFn = async (
+ vertexConstants: Record<string, GPUPipelineConstantValue>,
+ fragmentConstants: Record<string, GPUPipelineConstantValue>
+ ) => {
+ const descriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: vertexModule,
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: fragmentModule,
+ entryPoint: 'main',
+ targets: [{ format }],
+ constants: fragmentConstants,
+ },
+ primitive: {
+ topology: 'triangle-list',
+ frontFace: 'ccw',
+ cullMode: 'back',
+ },
+ };
+
+ return t.params.isAsync
+ ? t.device.createRenderPipelineAsync(descriptor)
+ : t.device.createRenderPipeline(descriptor);
+ };
+
+ const pipeline0 = await createPipelineFn(
+ t.params.vertexConstants0,
+ t.params.fragmentConstants0
+ );
+ const pipeline1 = await createPipelineFn(
+ t.params.vertexConstants1,
+ t.params.fragmentConstants1
+ );
+
+ const renderTarget0 = t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const renderTarget1 = t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+
+ const pass0 = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget0.createView(),
+ storeOp: 'store',
+ clearValue: {
+ r: kClearValueResult.R,
+ g: kClearValueResult.G,
+ b: kClearValueResult.B,
+ a: kClearValueResult.A,
+ },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass0.setPipeline(pipeline0);
+ pass0.draw(3);
+ pass0.end();
+
+ const pass1 = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget1.createView(),
+ storeOp: 'store',
+ clearValue: {
+ r: kClearValueResult.R,
+ g: kClearValueResult.G,
+ b: kClearValueResult.B,
+ a: kClearValueResult.A,
+ },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass1.setPipeline(pipeline1);
+ pass1.draw(3);
+ pass1.end();
+
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectSingleColor(renderTarget0, format, {
+ size: [1, 1, 1],
+ exp: t.params.expected0,
+ });
+ t.expectSingleColor(renderTarget1, format, {
+ size: [1, 1, 1],
+ exp: t.params.expected1,
+ });
+ });
+
+g.test('multi_entry_points')
+ .desc(
+ `Test that when the same module is shared by vertex and fragment shader, the constant values are still being used correctly.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [true, false])
+ .beginSubcases()
+ .combineWithParams([
+ {
+ expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 },
+ vertexConstants: { A: 4.0, B: 4.0 } as Record<string, GPUPipelineConstantValue>,
+ fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ {
+ expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 },
+ vertexConstants: {},
+ fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ {
+ expected: kClearValueResult,
+ vertexConstants: { A: -3.0 },
+ fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
+ string,
+ GPUPipelineConstantValue
+ >,
+ },
+ ])
+ )
+ .fn(async t => {
+ const format = 'bgra8unorm';
+ const module = t.device.createShaderModule({
+ code: `
+ override A: f32 = 3.0;
+ override B: f32 = 3.0;
+ override C: f32;
+ override D: f32;
+
+ @vertex fn vertexMain(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, A),
+ vec2<f32>(-1.0, -A),
+ vec2<f32>(B, 0.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+
+ @fragment fn fragmentMain()
+ -> @location(0) vec4<f32> {
+ return vec4<f32>(A, B, C, D);
+ }
+ `,
+ });
+ await t.ExpectShaderOutputWithConstants(
+ t.params.isAsync,
+ format,
+ t.params.expected,
+ {
+ module,
+ entryPoint: 'vertexMain',
+ constants: t.params.vertexConstants,
+ },
+ {
+ module,
+ entryPoint: 'fragmentMain',
+ constants: t.params.fragmentConstants,
+ targets: [{ format }],
+ }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/pipeline_output_targets.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/pipeline_output_targets.spec.ts
new file mode 100644
index 0000000000..729df6aca2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/pipeline_output_targets.spec.ts
@@ -0,0 +1,458 @@
+export const description = `
+- Test pipeline outputs with different color attachment number, formats, component counts, etc.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import {
+ kLimitInfo,
+ kRenderableColorTextureFormats,
+ kTextureFormatInfo,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { getFragmentShaderCodeWithOutput, getPlainTypeInfo } from '../../../util/shader.js';
+import { kTexelRepresentationInfo } from '../../../util/texture/texel_data.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+const kVertexShader = `
+@vertex fn main(
+@builtin(vertex_index) VertexIndex : u32
+) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -3.0),
+ vec2<f32>(3.0, 1.0),
+ vec2<f32>(-1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+}
+`;
+
+export const g = makeTestGroup(GPUTest);
+
+// Values to write into each attachment
+// We make values different for each attachment index and each channel
+// to make sure they didn't get mixed up
+const attachmentsIntWriteValues = [
+ { R: 1, G: 2, B: 3, A: 4 },
+ { R: 5, G: 6, B: 7, A: 8 },
+ { R: 9, G: 10, B: 11, A: 12 },
+ { R: 13, G: 14, B: 15, A: 16 },
+];
+const attachmentsFloatWriteValues = [
+ { R: 0.12, G: 0.34, B: 0.56, A: 0 },
+ { R: 0.78, G: 0.9, B: 0.19, A: 1 },
+ { R: 0.28, G: 0.37, B: 0.46, A: 0.3 },
+ { R: 0.55, G: 0.64, B: 0.73, A: 1 },
+];
+
+g.test('color,attachments')
+ .desc(`Test that pipeline with sparse color attachments write values correctly.`)
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine('attachmentCount', [2, 3, 4])
+ .filter(t => {
+ // We only need to test formats that have a valid color attachment bytes per sample.
+ const pixelByteCost = kTextureFormatInfo[t.format].renderTargetPixelByteCost;
+ return (
+ pixelByteCost !== undefined &&
+ pixelByteCost * t.attachmentCount <= kLimitInfo.maxColorAttachmentBytesPerSample.default
+ );
+ })
+ .expand('emptyAttachmentId', p => range(p.attachmentCount, i => i))
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, attachmentCount, emptyAttachmentId } = t.params;
+ const componentCount = kTexelRepresentationInfo[format].componentOrder.length;
+ const info = kTextureFormatInfo[format];
+
+ const writeValues =
+ info.sampleType === 'sint' || info.sampleType === 'uint'
+ ? attachmentsIntWriteValues
+ : attachmentsFloatWriteValues;
+
+ const renderTargets = range(attachmentCount, () =>
+ t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ );
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kVertexShader,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: getFragmentShaderCodeWithOutput(
+ range(attachmentCount, i =>
+ i === emptyAttachmentId
+ ? null
+ : {
+ values: [
+ writeValues[i].R,
+ writeValues[i].G,
+ writeValues[i].B,
+ writeValues[i].A,
+ ],
+ plainType: getPlainTypeInfo(info.sampleType),
+ componentCount,
+ }
+ )
+ ),
+ }),
+ entryPoint: 'main',
+ targets: range(attachmentCount, i => (i === emptyAttachmentId ? null : { format })),
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: range(attachmentCount, i =>
+ i === emptyAttachmentId
+ ? null
+ : {
+ view: renderTargets[i].createView(),
+ storeOp: 'store',
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 0.5 },
+ loadOp: 'clear',
+ }
+ ),
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ const promises = range(attachmentCount, i => {
+ if (i === emptyAttachmentId) {
+ return undefined;
+ }
+ return textureContentIsOKByT2B(
+ t,
+ { texture: renderTargets[i] },
+ [1, 1, 1],
+ {
+ expTexelView: TexelView.fromTexelsAsColors(format, coords => writeValues[i]),
+ },
+ {
+ maxIntDiff: 0,
+ maxDiffULPsForNormFormat: 1,
+ maxDiffULPsForFloatFormat: 1,
+ }
+ );
+ });
+ t.eventualExpectOK(Promise.all(promises));
+ });
+
+g.test('color,component_count')
+ .desc(
+ `Test that extra components of the output (e.g. f32, vec2<f32>, vec3<f32>, vec4<f32>) are discarded.`
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine('componentCount', [1, 2, 3, 4])
+ .filter(x => x.componentCount >= kTexelRepresentationInfo[x.format].componentOrder.length)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, componentCount } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ // expected RGBA values
+ // extra channels are discarded
+ const values = [0, 1, 0, 1];
+
+ const renderTarget = t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kVertexShader,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: getFragmentShaderCodeWithOutput([
+ {
+ values,
+ plainType: getPlainTypeInfo(info.sampleType),
+ componentCount,
+ },
+ ]),
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectSingleColor(renderTarget, format, {
+ size: [1, 1, 1],
+ exp: { R: values[0], G: values[1], B: values[2], A: values[3] },
+ });
+ });
+
+g.test('color,component_count,blend')
+ .desc(
+ `Test that blending behaves correctly when:
+- fragment output has no alpha, but the src alpha is not used for the blend operation indicated by blend factors
+- attachment format has no alpha, and the dst alpha should be assumed as 1
+
+The attachment has a load value of [1, 0, 0, 1]
+`
+ )
+ .params(u =>
+ u
+ .combine('format', ['r8unorm', 'rg8unorm', 'rgba8unorm', 'bgra8unorm'] as const)
+ .beginSubcases()
+ // _result is expected values in the color attachment (extra channels are discarded)
+ // output is the fragment shader output vector
+ // 0.498 -> 0x7f, 0.502 -> 0x80
+ .combineWithParams([
+ // fragment output has no alpha
+ {
+ _result: [0, 0, 0, 0],
+ output: [0],
+ colorSrcFactor: 'one',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0, 0, 0, 0],
+ output: [0],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [1, 0, 0, 0],
+ output: [0],
+ colorSrcFactor: 'one-minus-dst-alpha',
+ colorDstFactor: 'dst-alpha',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'one',
+ },
+ {
+ _result: [0.498, 0, 0, 0],
+ output: [0.498],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'one',
+ },
+ {
+ _result: [0, 1, 0, 0],
+ output: [0, 1],
+ colorSrcFactor: 'one',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0, 1, 0, 0],
+ output: [0, 1],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [1, 0, 0, 0],
+ output: [0, 1],
+ colorSrcFactor: 'one-minus-dst-alpha',
+ colorDstFactor: 'dst-alpha',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'one',
+ },
+ {
+ _result: [0, 1, 0, 0],
+ output: [0, 1, 0],
+ colorSrcFactor: 'one',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0, 1, 0, 0],
+ output: [0, 1, 0],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [1, 0, 0, 0],
+ output: [0, 1, 0],
+ colorSrcFactor: 'one-minus-dst-alpha',
+ colorDstFactor: 'dst-alpha',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'one',
+ },
+ // fragment output has alpha
+ {
+ _result: [0.502, 1, 0, 0.498],
+ output: [0, 1, 0, 0.498],
+ colorSrcFactor: 'one',
+ colorDstFactor: 'one-minus-src-alpha',
+ alphaSrcFactor: 'one',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0.502, 0.498, 0, 0.498],
+ output: [0, 1, 0, 0.498],
+ colorSrcFactor: 'src-alpha',
+ colorDstFactor: 'one-minus-src-alpha',
+ alphaSrcFactor: 'one',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0, 1, 0, 0.498],
+ output: [0, 1, 0, 0.498],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'one',
+ alphaDstFactor: 'zero',
+ },
+ {
+ _result: [0, 1, 0, 0.498],
+ output: [0, 1, 0, 0.498],
+ colorSrcFactor: 'dst-alpha',
+ colorDstFactor: 'zero',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'src',
+ },
+ {
+ _result: [1, 0, 0, 1],
+ output: [0, 1, 0, 0.498],
+ colorSrcFactor: 'one-minus-dst-alpha',
+ colorDstFactor: 'dst-alpha',
+ alphaSrcFactor: 'zero',
+ alphaDstFactor: 'dst-alpha',
+ },
+ ] as const)
+ .filter(x => x.output.length >= kTexelRepresentationInfo[x.format].componentOrder.length)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ format,
+ _result,
+ output,
+ colorSrcFactor,
+ colorDstFactor,
+ alphaSrcFactor,
+ alphaDstFactor,
+ } = t.params;
+ const componentCount = output.length;
+ const info = kTextureFormatInfo[format];
+
+ const renderTarget = t.device.createTexture({
+ format,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kVertexShader,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: getFragmentShaderCodeWithOutput([
+ {
+ values: output,
+ plainType: getPlainTypeInfo(info.sampleType),
+ componentCount,
+ },
+ ]),
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ format,
+ blend: {
+ color: {
+ srcFactor: colorSrcFactor,
+ dstFactor: colorDstFactor,
+ operation: 'add',
+ },
+ alpha: {
+ srcFactor: alphaSrcFactor,
+ dstFactor: alphaDstFactor,
+ operation: 'add',
+ },
+ },
+ },
+ ],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectSingleColor(renderTarget, format, {
+ size: [1, 1, 1],
+ exp: { R: _result[0], G: _result[1], B: _result[2], A: _result[3] },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/primitive_topology.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/primitive_topology.spec.ts
new file mode 100644
index 0000000000..f9c89b5a28
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/primitive_topology.spec.ts
@@ -0,0 +1,498 @@
+export const description = `Test primitive topology rendering.
+
+Draw a primitive using 6 vertices with each topology and check if the pixel is covered.
+
+Vertex sequence and coordinates are the same for each topology:
+ - Vertex buffer = [v1, v2, v3, v4, v5, v6]
+ - Topology = [point-list, line-list, line-strip, triangle-list, triangle-strip]
+
+Test locations are framebuffer coordinates:
+ - Pixel value { valid: green, invalid: black, format: 'rgba8unorm'}
+ - Test point is valid if the pixel value equals the covered pixel value at the test location.
+ - Primitive restart occurs for strips (line-strip and triangle-strip) between [v3, v4].
+
+ Topology: point-list Valid test location(s) Invalid test location(s)
+
+ v2 v4 v6 Every vertex. Line-strip locations.
+ Triangle-list locations.
+ Triangle-strip locations.
+
+ v1 v3 v5
+
+ Topology: line-list (3 lines)
+
+ v2 v4 v6 Center of three line segments: Line-strip locations.
+ * * * {v1,V2}, {v3,v4}, and {v4,v5}. Triangle-list locations.
+ * * * Triangle-strip locations.
+ * * *
+ v1 v3 v5
+
+ Topology: line-strip (5 lines)
+
+ v2 v4 v6
+ ** ** *
+ * * * * * Line-list locations Triangle-list locations.
+ * ** ** + Center of two line segments: Triangle-strip locations.
+ v1 v3 v5 {v2,v3} and {v4,v5}.
+ With primitive restart:
+ Line segment {v3, v4}.
+
+ Topology: triangle-list (2 triangles)
+
+ v2 v4 v6
+ ** ****** Center of two triangle(s): Triangle-strip locations.
+ **** **** {v1,v2,v3} and {v4,v5,v6}.
+ ****** **
+ v1 v3 v5
+
+ Topology: triangle-strip (4 triangles)
+
+ v2 v4 v6
+ ** ****** ** ****** Triangle-list locations None.
+ **** **** **** **** + Center of two triangle(s):
+ ****** ** ****** ** {v2,v3,v4} and {v3,v4,v5}. With primitive restart:
+ v1 v3 v5 Triangle {v2, v3, v4}
+ and {v3, v4, v5}.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+const kRTSize: number = 56;
+const kColorFormat = 'rgba8unorm';
+const kValidPixelColor = new Uint8Array([0x00, 0xff, 0x00, 0xff]); // green
+const kInvalidPixelColor = new Uint8Array([0x00, 0x00, 0x00, 0x00]); // black
+
+class Point2D {
+ x: number;
+ y: number;
+ z: number;
+ w: number;
+
+ constructor(x: number, y: number) {
+ this.x = x;
+ this.y = y;
+ this.z = 0;
+ this.w = 1;
+ }
+
+ toNDC(): Point2D {
+ // NDC coordinate space is y-up, so we negate the y mapping.
+ // To ensure the resulting vertex in NDC will be placed at the center of the pixel, we
+ // must offset by the pixel coordinates or 0.5.
+ return new Point2D((2 * (this.x + 0.5)) / kRTSize - 1, (-2 * (this.y + 0.5)) / kRTSize + 1);
+ }
+
+ static getMidpoint(a: Point2D, b: Point2D) {
+ return new Point2D((a.x + b.x) / 2, (a.y + b.y) / 2);
+ }
+
+ static getCentroid(a: Point2D, b: Point2D, c: Point2D) {
+ return new Point2D((a.x + b.x + c.x) / 3, (a.y + b.y + c.y) / 3);
+ }
+}
+
+interface TestLocation {
+ location: Point2D;
+ color: Uint8Array;
+}
+
+const VertexLocations = [
+ new Point2D(8, 24), // v1
+ new Point2D(16, 8), // v2
+ new Point2D(24, 24), // v3
+ new Point2D(32, 8), // v4
+ new Point2D(40, 24), // v5
+ new Point2D(48, 8), // v6
+];
+
+function getPointTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Test points are always equal to vertex locations.
+ const testLocations: TestLocation[] = [];
+ for (const location of VertexLocations) {
+ testLocations.push({ location, color: expectedColor });
+ }
+ return testLocations;
+}
+
+function getLineTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Midpoints of 3 line segments
+ return [
+ {
+ // Line {v1, v2}
+ location: Point2D.getMidpoint(VertexLocations[0], VertexLocations[1]),
+ color: expectedColor,
+ },
+ {
+ // Line {v3, v4}
+ location: Point2D.getMidpoint(VertexLocations[2], VertexLocations[3]),
+ color: expectedColor,
+ },
+ {
+ // Line {v5, v6}
+ location: Point2D.getMidpoint(VertexLocations[4], VertexLocations[5]),
+ color: expectedColor,
+ },
+ ];
+}
+
+function getPrimitiveRestartLineTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Midpoints of 2 line segments
+ return [
+ {
+ // Line {v1, v2}
+ location: Point2D.getMidpoint(VertexLocations[0], VertexLocations[1]),
+ color: expectedColor,
+ },
+ {
+ // Line {v5, v6}
+ location: Point2D.getMidpoint(VertexLocations[4], VertexLocations[5]),
+ color: expectedColor,
+ },
+ ];
+}
+
+function getLineStripTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Midpoints of 2 line segments
+ return [
+ {
+ // Line {v2, v3}
+ location: Point2D.getMidpoint(VertexLocations[1], VertexLocations[2]),
+ color: expectedColor,
+ },
+ {
+ // Line {v4, v5}
+ location: Point2D.getMidpoint(VertexLocations[3], VertexLocations[4]),
+ color: expectedColor,
+ },
+ ];
+}
+
+function getTriangleListTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Center of two triangles
+ return [
+ {
+ // Triangle {v1, v2, v3}
+ location: Point2D.getCentroid(VertexLocations[0], VertexLocations[1], VertexLocations[2]),
+ color: expectedColor,
+ },
+ {
+ // Triangle {v4, v5, v6}
+ location: Point2D.getCentroid(VertexLocations[3], VertexLocations[4], VertexLocations[5]),
+ color: expectedColor,
+ },
+ ];
+}
+
+function getTriangleStripTestLocations(expectedColor: Uint8Array): TestLocation[] {
+ // Center of two triangles
+ return [
+ {
+ // Triangle {v2, v3, v4}
+ location: Point2D.getCentroid(VertexLocations[1], VertexLocations[2], VertexLocations[3]),
+ color: expectedColor,
+ },
+ {
+ // Triangle {v3, v4, v5}
+ location: Point2D.getCentroid(VertexLocations[2], VertexLocations[3], VertexLocations[4]),
+ color: expectedColor,
+ },
+ ];
+}
+
+function getDefaultTestLocations({
+ topology,
+ primitiveRestart = false,
+ invalidateLastInList = false,
+}: {
+ topology: GPUPrimitiveTopology;
+ primitiveRestart?: boolean;
+ invalidateLastInList?: boolean;
+}) {
+ function maybeInvalidateLast(locations: TestLocation[]) {
+ if (!invalidateLastInList) return locations;
+
+ return locations.map((tl, i) => {
+ if (i === locations.length - 1) {
+ return {
+ location: tl.location,
+ color: kInvalidPixelColor,
+ };
+ } else {
+ return tl;
+ }
+ });
+ }
+
+ let testLocations: TestLocation[];
+ switch (topology) {
+ case 'point-list':
+ testLocations = [
+ ...getPointTestLocations(kValidPixelColor),
+ ...getLineStripTestLocations(kInvalidPixelColor),
+ ...getTriangleListTestLocations(kInvalidPixelColor),
+ ...getTriangleStripTestLocations(kInvalidPixelColor),
+ ];
+ break;
+ case 'line-list':
+ testLocations = [
+ ...maybeInvalidateLast(getLineTestLocations(kValidPixelColor)),
+ ...getLineStripTestLocations(kInvalidPixelColor),
+ ...getTriangleListTestLocations(kInvalidPixelColor),
+ ...getTriangleStripTestLocations(kInvalidPixelColor),
+ ];
+ break;
+ case 'line-strip':
+ testLocations = [
+ ...(primitiveRestart
+ ? getPrimitiveRestartLineTestLocations(kValidPixelColor)
+ : getLineTestLocations(kValidPixelColor)),
+ ...getLineStripTestLocations(kValidPixelColor),
+ ...getTriangleListTestLocations(kInvalidPixelColor),
+ ...getTriangleStripTestLocations(kInvalidPixelColor),
+ ];
+ break;
+ case 'triangle-list':
+ testLocations = [
+ ...maybeInvalidateLast(getTriangleListTestLocations(kValidPixelColor)),
+ ...getTriangleStripTestLocations(kInvalidPixelColor),
+ ];
+ break;
+ case 'triangle-strip':
+ testLocations = [
+ ...getTriangleListTestLocations(kValidPixelColor),
+ ...getTriangleStripTestLocations(primitiveRestart ? kInvalidPixelColor : kValidPixelColor),
+ ];
+ break;
+ }
+ return testLocations;
+}
+
+function generateVertexBuffer(vertexLocations: Point2D[]): Float32Array {
+ const vertexCoords = new Float32Array(vertexLocations.length * 4);
+ for (let i = 0; i < vertexLocations.length; i++) {
+ const point = vertexLocations[i].toNDC();
+ vertexCoords[i * 4 + 0] = point.x;
+ vertexCoords[i * 4 + 1] = point.y;
+ vertexCoords[i * 4 + 2] = point.z;
+ vertexCoords[i * 4 + 3] = point.w;
+ }
+ return vertexCoords;
+}
+
+const kDefaultDrawCount = 6;
+class PrimitiveTopologyTest extends GPUTest {
+ makeAttachmentTexture(): GPUTexture {
+ return this.device.createTexture({
+ format: kColorFormat,
+ size: { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ }
+
+ run({
+ topology,
+ indirect,
+ testLocations,
+ primitiveRestart = false,
+ drawCount = kDefaultDrawCount,
+ }: {
+ topology: GPUPrimitiveTopology;
+ indirect: boolean;
+ testLocations: TestLocation[];
+ primitiveRestart?: boolean;
+ drawCount?: number;
+ }): void {
+ const colorAttachment = this.makeAttachmentTexture();
+
+ // Color load operator will clear color attachment to zero.
+ const encoder = this.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ let stripIndexFormat = undefined;
+ if (topology === 'triangle-strip' || topology === 'line-strip') {
+ stripIndexFormat = 'uint32' as const;
+ }
+
+ // Draw a primitive using 6 vertices based on the type.
+ // Pixels are generated based on vertex position.
+ // If point, 1 pixel is generated at each vertex location.
+ // Otherwise, >1 pixels could be generated.
+ // Output color is solid green.
+ renderPass.setPipeline(
+ this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @location(0) pos : vec4<f32>
+ ) -> @builtin(position) vec4<f32> {
+ return pos;
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT,
+ attributes: [
+ {
+ format: 'float32x4',
+ offset: 0,
+ shaderLocation: 0,
+ },
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: kColorFormat }],
+ },
+ primitive: {
+ topology,
+ stripIndexFormat,
+ },
+ })
+ );
+
+ // Create vertices for the primitive in a vertex buffer and bind it.
+ const vertexCoords = generateVertexBuffer(VertexLocations);
+ const vertexBuffer = this.makeBufferWithContents(vertexCoords, GPUBufferUsage.VERTEX);
+ renderPass.setVertexBuffer(0, vertexBuffer);
+
+ // Restart the strip between [v3, <restart>, v4].
+ if (primitiveRestart) {
+ const indexBuffer = this.makeBufferWithContents(
+ new Uint32Array([0, 1, 2, -1, 3, 4, 5]),
+ GPUBufferUsage.INDEX
+ );
+ renderPass.setIndexBuffer(indexBuffer, 'uint32');
+
+ if (indirect) {
+ renderPass.drawIndexedIndirect(
+ this.makeBufferWithContents(
+ new Uint32Array([drawCount + 1, 1, 0, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ ),
+ 0
+ );
+ } else {
+ renderPass.drawIndexed(drawCount + 1); // extra index for restart
+ }
+ } else {
+ if (indirect) {
+ renderPass.drawIndirect(
+ this.makeBufferWithContents(
+ new Uint32Array([drawCount, 1, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ ),
+ 0
+ );
+ } else {
+ renderPass.draw(drawCount);
+ }
+ }
+
+ renderPass.end();
+
+ this.device.queue.submit([encoder.finish()]);
+
+ for (const testPixel of testLocations) {
+ this.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ kColorFormat,
+ { x: testPixel.location.x, y: testPixel.location.y },
+ { exp: testPixel.color }
+ );
+ }
+ }
+}
+
+export const g = makeTestGroup(PrimitiveTopologyTest);
+
+const topologies: GPUPrimitiveTopology[] = [
+ 'point-list',
+ 'line-list',
+ 'line-strip',
+ 'triangle-list',
+ 'triangle-strip',
+];
+
+g.test('basic')
+ .desc(
+ `Compute test locations for valid and invalid pixels for each topology.
+ If the primitive covers the pixel, the color value will be |kValidPixelColor|.
+ Otherwise, a non-covered pixel will be |kInvalidPixelColor|.
+
+ Params:
+ - topology= {...all topologies}
+ - indirect= {true, false}
+ - primitiveRestart= { true, false } - always false for non-strip topologies
+ `
+ )
+ .params(u =>
+ u //
+ .combine('topology', topologies)
+ .combine('indirect', [false, true])
+ .combine('primitiveRestart', [false, true])
+ .unless(
+ p => p.primitiveRestart && p.topology !== 'line-strip' && p.topology !== 'triangle-strip'
+ )
+ )
+ .fn(t => {
+ t.run({
+ ...t.params,
+ testLocations: getDefaultTestLocations(t.params),
+ });
+ });
+
+g.test('unaligned_vertex_count')
+ .desc(
+ `Test that drawing with a number of vertices that's not a multiple of the vertices a given primitive list topology is not an error. The last primitive is not drawn.
+
+ Params:
+ - topology= {line-list, triangle-list}
+ - indirect= {true, false}
+ - drawCount - number of vertices to draw. A value smaller than the test's default of ${kDefaultDrawCount}.
+ One smaller for line-list. One or two smaller for triangle-list.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('topology', ['line-list', 'triangle-list'] as const)
+ .combine('indirect', [false, true])
+ .expand('drawCount', function* (p) {
+ switch (p.topology) {
+ case 'line-list':
+ yield kDefaultDrawCount - 1;
+ break;
+ case 'triangle-list':
+ yield kDefaultDrawCount - 1;
+ yield kDefaultDrawCount - 2;
+ break;
+ }
+ })
+ )
+ .fn(t => {
+ const testLocations = getDefaultTestLocations({ ...t.params, invalidateLastInList: true });
+ t.run({
+ ...t.params,
+ testLocations,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts
new file mode 100644
index 0000000000..4799e2a572
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/sample_mask.spec.ts
@@ -0,0 +1,519 @@
+export const description = `
+Tests that the final sample mask is the logical AND of all the relevant masks.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { TypeF32, TypeU32 } from '../../../util/conversion.js';
+import { makeTextureWithContents } from '../../../util/texture.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+
+const kColors = [
+ // Red
+ new Uint8Array([0xff, 0, 0, 0xff]),
+ // Green
+ new Uint8Array([0, 0xff, 0, 0xff]),
+ // Blue
+ new Uint8Array([0, 0, 0xff, 0xff]),
+ // Yellow
+ new Uint8Array([0xff, 0xff, 0, 0xff]),
+];
+
+const kDepthClearValue = 1.0;
+const kDepthWriteValue = 0.0;
+const kStencilClearValue = 0;
+const kStencilReferenceValue = 0xff;
+
+// Format of the render target and resolve target
+const format = 'rgba8unorm';
+
+// Format of depth stencil attachment
+const depthStencilFormat = 'depth24plus-stencil8';
+
+const kRenderTargetSize = 1;
+
+function hasSample(
+ rasterizationMask: number,
+ sampleMask: number,
+ fragmentShaderOutputMask: number,
+ sampleIndex: number = 0
+): boolean {
+ return (rasterizationMask & sampleMask & fragmentShaderOutputMask & (1 << sampleIndex)) > 0;
+}
+
+class F extends GPUTest {
+ async GetTargetTexture(
+ sampleCount: number,
+ rasterizationMask: number,
+ sampleMask: number,
+ fragmentShaderOutputMask: number
+ ): Promise<{ color: GPUTexture; depthStencil: GPUTexture }> {
+ // Create a 2x2 color texture to sample from
+ // texel 0 - Red
+ // texel 1 - Green
+ // texel 2 - Blue
+ // texel 3 - Yellow
+ const kSampleTextureSize = 2;
+ const sampleTexture = makeTextureWithContents(
+ this.device,
+ TexelView.fromTexelsAsBytes(format, coord => {
+ const id = coord.x + coord.y * kSampleTextureSize;
+ return kColors[id];
+ }),
+ {
+ size: [kSampleTextureSize, kSampleTextureSize, 1],
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ }
+ );
+
+ const sampler = this.device.createSampler({
+ magFilter: 'nearest',
+ minFilter: 'nearest',
+ });
+
+ const fragmentMaskUniformBuffer = this.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
+ });
+ this.trackForCleanup(fragmentMaskUniformBuffer);
+ this.device.queue.writeBuffer(
+ fragmentMaskUniformBuffer,
+ 0,
+ new Uint32Array([fragmentShaderOutputMask])
+ );
+
+ const pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct VertexOutput {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) @interpolate(perspective, sample) fragUV : vec2<f32>,
+ }
+
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 30>(
+ // center quad
+ // only covers pixel center which is sample point when sampleCount === 1
+ // small enough to avoid covering any multi sample points
+ vec2<f32>( 0.2, 0.2),
+ vec2<f32>( 0.2, -0.2),
+ vec2<f32>(-0.2, -0.2),
+ vec2<f32>( 0.2, 0.2),
+ vec2<f32>(-0.2, -0.2),
+ vec2<f32>(-0.2, 0.2),
+
+ // Sub quads are representing rasterization mask and
+ // are slightly scaled to avoid covering the pixel center
+
+ // top-left quad
+ vec2<f32>( -0.01, 1.0),
+ vec2<f32>( -0.01, 0.01),
+ vec2<f32>(-1.0, 0.01),
+ vec2<f32>( -0.01, 1.0),
+ vec2<f32>(-1.0, 0.01),
+ vec2<f32>(-1.0, 1.0),
+
+ // top-right quad
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(1.0, 0.01),
+ vec2<f32>(0.01, 0.01),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.01, 0.01),
+ vec2<f32>(0.01, 1.0),
+
+ // bottom-left quad
+ vec2<f32>( -0.01, -0.01),
+ vec2<f32>( -0.01, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( -0.01, -0.01),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, -0.01),
+
+ // bottom-right quad
+ vec2<f32>(1.0, -0.01),
+ vec2<f32>(1.0, -1.0),
+ vec2<f32>(0.01, -1.0),
+ vec2<f32>(1.0, -0.01),
+ vec2<f32>(0.01, -1.0),
+ vec2<f32>(0.01, -0.01)
+ );
+
+ var uv = array<vec2<f32>, 30>(
+ // center quad
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(0.0, 0.0),
+
+ // top-left quad (texel 0)
+ vec2<f32>(0.5, 0.0),
+ vec2<f32>(0.5, 0.5),
+ vec2<f32>(0.0, 0.5),
+ vec2<f32>(0.5, 0.0),
+ vec2<f32>(0.0, 0.5),
+ vec2<f32>(0.0, 0.0),
+
+ // top-right quad (texel 1)
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 0.5),
+ vec2<f32>(0.5, 0.5),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.5, 0.5),
+ vec2<f32>(0.5, 0.0),
+
+ // bottom-left quad (texel 2)
+ vec2<f32>(0.5, 0.5),
+ vec2<f32>(0.5, 1.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(0.5, 0.5),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(0.0, 0.5),
+
+ // bottom-right quad (texel 3)
+ vec2<f32>(1.0, 0.5),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.5, 1.0),
+ vec2<f32>(1.0, 0.5),
+ vec2<f32>(0.5, 1.0),
+ vec2<f32>(0.5, 0.5)
+ );
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], ${kDepthWriteValue}, 1.0);
+ output.fragUV = uv[VertexIndex];
+ return output;
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var mySampler: sampler;
+ @group(0) @binding(1) var myTexture: texture_2d<f32>;
+ @group(0) @binding(2) var<uniform> fragMask: u32;
+
+ struct FragmentOutput {
+ @builtin(sample_mask) mask : u32,
+ @location(0) color : vec4<f32>,
+ }
+
+ @fragment
+ fn main(@location(0) @interpolate(perspective, sample) fragUV: vec2<f32>) -> FragmentOutput {
+ return FragmentOutput(fragMask, textureSample(myTexture, mySampler, fragUV));
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: { topology: 'triangle-list' },
+ multisample: {
+ count: sampleCount,
+ mask: sampleMask,
+ alphaToCoverageEnabled: false,
+ },
+ depthStencil: {
+ format: depthStencilFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'always',
+
+ stencilFront: {
+ compare: 'always',
+ passOp: 'replace',
+ },
+ stencilBack: {
+ compare: 'always',
+ passOp: 'replace',
+ },
+ },
+ });
+
+ const uniformBindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: sampler,
+ },
+ {
+ binding: 1,
+ resource: sampleTexture.createView(),
+ },
+ {
+ binding: 2,
+ resource: {
+ buffer: fragmentMaskUniformBuffer,
+ },
+ },
+ ],
+ });
+
+ const renderTargetTexture = this.device.createTexture({
+ format,
+ size: {
+ width: kRenderTargetSize,
+ height: kRenderTargetSize,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount,
+ mipLevelCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ });
+ const resolveTargetTexture =
+ sampleCount === 1
+ ? null
+ : this.device.createTexture({
+ format,
+ size: {
+ width: kRenderTargetSize,
+ height: kRenderTargetSize,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: 1,
+ mipLevelCount: 1,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const depthStencilTexture = this.device.createTexture({
+ size: {
+ width: kRenderTargetSize,
+ height: kRenderTargetSize,
+ },
+ format: depthStencilFormat,
+ sampleCount,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTargetTexture.createView(),
+ resolveTarget: resolveTargetTexture?.createView(),
+
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: depthStencilTexture.createView(),
+ depthClearValue: kDepthClearValue,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: kStencilClearValue,
+ stencilLoadOp: 'clear',
+ stencilStoreOp: 'store',
+ },
+ };
+ const commandEncoder = this.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.setBindGroup(0, uniformBindGroup);
+ passEncoder.setStencilReference(kStencilReferenceValue);
+
+ if (sampleCount === 1) {
+ if ((rasterizationMask & 1) !== 0) {
+ // draw center quad
+ passEncoder.draw(6);
+ }
+ } else {
+ assert(sampleCount === 4);
+ if ((rasterizationMask & 1) !== 0) {
+ // draw top-left quad
+ passEncoder.draw(6, 1, 6);
+ }
+ if ((rasterizationMask & 2) !== 0) {
+ // draw top-right quad
+ passEncoder.draw(6, 1, 12);
+ }
+ if ((rasterizationMask & 4) !== 0) {
+ // draw bottom-left quad
+ passEncoder.draw(6, 1, 18);
+ }
+ if ((rasterizationMask & 8) !== 0) {
+ // draw bottom-right quad
+ passEncoder.draw(6, 1, 24);
+ }
+ }
+ passEncoder.end();
+ this.device.queue.submit([commandEncoder.finish()]);
+
+ return {
+ color: renderTargetTexture,
+ depthStencil: depthStencilTexture,
+ };
+ }
+
+ CheckColorAttachmentResult(
+ texture: GPUTexture,
+ sampleCount: number,
+ rasterizationMask: number,
+ sampleMask: number,
+ fragmentShaderOutputMask: number
+ ) {
+ const buffer = this.copySinglePixelTextureToBufferUsingComputePass(
+ TypeF32, // correspond to 'rgba8unorm' format
+ 4,
+ texture.createView(),
+ sampleCount
+ );
+
+ const expectedDstData = new Float32Array(sampleCount * 4);
+ if (sampleCount === 1) {
+ if (hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask)) {
+ // Texel 3 is sampled at the pixel center
+ expectedDstData[0] = kColors[3][0] / 0xff;
+ expectedDstData[1] = kColors[3][1] / 0xff;
+ expectedDstData[2] = kColors[3][2] / 0xff;
+ expectedDstData[3] = kColors[3][3] / 0xff;
+ }
+ } else {
+ for (let i = 0; i < sampleCount; i++) {
+ if (hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask, i)) {
+ const o = i * 4;
+ expectedDstData[o + 0] = kColors[i][0] / 0xff;
+ expectedDstData[o + 1] = kColors[i][1] / 0xff;
+ expectedDstData[o + 2] = kColors[i][2] / 0xff;
+ expectedDstData[o + 3] = kColors[i][3] / 0xff;
+ }
+ }
+ }
+
+ this.expectGPUBufferValuesEqual(buffer, expectedDstData);
+ }
+
+ CheckDepthStencilResult(
+ aspect: 'depth-only' | 'stencil-only',
+ depthStencilTexture: GPUTexture,
+ sampleCount: number,
+ rasterizationMask: number,
+ sampleMask: number,
+ fragmentShaderOutputMask: number
+ ) {
+ const buffer = this.copySinglePixelTextureToBufferUsingComputePass(
+ // Use f32 as the scalar type for depth (depth24plus, depth32float)
+ // Use u32 as the scalar type for stencil (stencil8)
+ aspect === 'depth-only' ? TypeF32 : TypeU32,
+ 1,
+ depthStencilTexture.createView({ aspect }),
+ sampleCount
+ );
+
+ const expectedDstData =
+ aspect === 'depth-only' ? new Float32Array(sampleCount) : new Uint32Array(sampleCount);
+ for (let i = 0; i < sampleCount; i++) {
+ const s = hasSample(rasterizationMask, sampleMask, fragmentShaderOutputMask, i);
+ if (aspect === 'depth-only') {
+ expectedDstData[i] = s ? kDepthWriteValue : kDepthClearValue;
+ } else {
+ expectedDstData[i] = s ? kStencilReferenceValue : kStencilClearValue;
+ }
+ }
+ this.expectGPUBufferValuesEqual(buffer, expectedDstData);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('final_output')
+ .desc(
+ `
+Tests that the final sample mask is the logical AND of all the relevant masks -- meaning that the samples
+not included in the final mask are discarded on any attachments including
+- color outputs
+- depth tests
+- stencil operations
+
+The test draws 0/1/1+ textured quads of which each sample in the standard 4-sample pattern results in a different color:
+- Sample 0, Texel 0, top-left: Red
+- Sample 1, Texel 1, top-left: Green
+- Sample 2, Texel 2, top-left: Blue
+- Sample 3, Texel 3, top-left: Yellow
+
+The test checks each sample value of the render target texture and depth stencil texture using a compute pass to
+textureLoad each sample index from the texture and write to a storage buffer to compare with expected values.
+
+- for sampleCount = { 1, 4 } and various combinations of:
+ - rasterization mask = { 0, ..., 2 ** sampleCount - 1 }
+ - sample mask = { 0, 0b0001, 0b0010, 0b0111, 0b1011, 0b1101, 0b1110, 0b1111, 0b11110 }
+ - fragment shader output @builtin(sample_mask) = { 0, 0b0001, 0b0010, 0b0111, 0b1011, 0b1101, 0b1110, 0b1111, 0b11110 }
+- [choosing 0b11110 because the 5th bit should be ignored]
+`
+ )
+ .params(u =>
+ u
+ .combine('sampleCount', [1, 4] as const)
+ .expand('rasterizationMask', function* (p) {
+ for (let i = 0, len = 2 ** p.sampleCount - 1; i <= len; i++) {
+ yield i;
+ }
+ })
+ .beginSubcases()
+ .combine('sampleMask', [
+ 0,
+ 0b0001,
+ 0b0010,
+ 0b0111,
+ 0b1011,
+ 0b1101,
+ 0b1110,
+ 0b1111,
+ 0b11110,
+ ] as const)
+ .combine('fragmentShaderOutputMask', [
+ 0,
+ 0b0001,
+ 0b0010,
+ 0b0111,
+ 0b1011,
+ 0b1101,
+ 0b1110,
+ 0b1111,
+ 0b11110,
+ ] as const)
+ )
+ .fn(async t => {
+ const { sampleCount, rasterizationMask, sampleMask, fragmentShaderOutputMask } = t.params;
+
+ const { color, depthStencil } = await t.GetTargetTexture(
+ sampleCount,
+ rasterizationMask,
+ sampleMask,
+ fragmentShaderOutputMask
+ );
+
+ t.CheckColorAttachmentResult(
+ color,
+ sampleCount,
+ rasterizationMask,
+ sampleMask,
+ fragmentShaderOutputMask
+ );
+
+ t.CheckDepthStencilResult(
+ 'depth-only',
+ depthStencil,
+ sampleCount,
+ rasterizationMask,
+ sampleMask,
+ fragmentShaderOutputMask
+ );
+
+ t.CheckDepthStencilResult(
+ 'stencil-only',
+ depthStencil,
+ sampleCount,
+ rasterizationMask,
+ sampleMask,
+ fragmentShaderOutputMask
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/vertex_only_render_pipeline.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/vertex_only_render_pipeline.spec.ts
new file mode 100644
index 0000000000..ef2d108f1e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/render_pipeline/vertex_only_render_pipeline.spec.ts
@@ -0,0 +1,29 @@
+export const description = `
+Test vertex-only render pipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+class F extends GPUTest {}
+
+export const g = makeTestGroup(F);
+
+g.test('draw_depth_and_stencil_with_vertex_only_pipeline')
+ .desc(
+ `
+TODO:
+- Test drawing depth and stencil with vertex-only render pipelines by
+ 1. Create a color attachment and depth-stencil attachment of 4 pixels in a line, clear the color
+ to RGBA(0.0, 0.0, 0.0, 0.0), depth to 0.0 and stencil to 0x0
+ 2. Use a depth and stencil test disabled vertex-only render pipeline to modify the depth of middle
+ 2 pixels to 0.5, while leaving stencil unchanged
+ 3. Use another depth and stencil test disabled vertex-only render pipeline to modify the stencil
+ of right 2 pixels to 0x1, while leaving depth unchanged
+ 4. Use a complete render pipeline to draw all 4 pixels with color RGBA(0.0, 1.0, 0.0, 1.0), but
+ with depth test requiring depth no less than 0.5 and stencil test requiring stencil equals to 0x1
+ 5. Validate that only the third pixel is of color RGBA(0.0, 1.0, 0.0, 1.0), and all other pixels
+ are RGBA(0.0, 0.0, 0.0, 0.0).
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/basic.spec.ts
new file mode 100644
index 0000000000..d58f0c8d4a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/basic.spec.ts
@@ -0,0 +1,353 @@
+export const description = `
+Basic command buffer rendering tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { now } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { checkElementsEqual } from '../../../util/check_contents.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('clear').fn(async t => {
+ const dst = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const colorAttachment = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dst, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(dst, new Uint8Array([0x00, 0xff, 0x00, 0xff]));
+});
+
+g.test('fullscreen_quad').fn(async t => {
+ const dst = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const colorAttachment = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -3.0),
+ vec2<f32>(3.0, 1.0),
+ vec2<f32>(-1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dst, bytesPerRow: 256 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(dst, new Uint8Array([0x00, 0xff, 0x00, 0xff]));
+});
+
+g.test('large_draw')
+ .desc(
+ `Test reasonably-sized large {draw, drawIndexed} (see also stress tests).
+
+ Tests that draw calls behave reasonably with large vertex counts for
+ non-indexed draws, large index counts for indexed draws, and large instance
+ counts in both cases. Various combinations of these counts are tested with
+ both direct and indirect draw calls.
+
+ Draw call sizes are increased incrementally over these parameters until we the
+ run out of values or completion of a draw call exceeds a fixed time limit of
+ 100ms.
+
+ To validate that the drawn vertices actually made it though the pipeline on
+ each draw call, we render a 3x3 target with the positions of the first and
+ last vertices of the first and last instances in different respective corners,
+ and everything else positioned to cover only one of the intermediate
+ fragments. If the output image is completely yellow, then we can reasonably
+ infer that all vertices were drawn.
+
+ Params:
+ - indexed= {true, false} - whether to test indexed or non-indexed draw calls
+ - indirect= {true, false} - whether to use indirect or direct draw calls`
+ )
+ .params(u =>
+ u //
+ .combine('indexed', [true, false])
+ .combine('indirect', [true, false])
+ )
+ .fn(async t => {
+ const { indexed, indirect } = t.params;
+
+ const kBytesPerRow = 256;
+ const dst = t.device.createBuffer({
+ size: 3 * kBytesPerRow,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const paramsBuffer = t.device.createBuffer({
+ size: 8,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+
+ const indirectBuffer = t.device.createBuffer({
+ size: 20,
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
+ });
+ const writeIndirectParams = (count: number, instanceCount: number) => {
+ const params = new Uint32Array(5);
+ params[0] = count; // Vertex or index count
+ params[1] = instanceCount;
+ params[2] = 0; // First vertex or index
+ params[3] = 0; // First instance (non-indexed) or base vertex (indexed)
+ params[4] = 0; // First instance (indexed)
+ t.device.queue.writeBuffer(indirectBuffer, 0, params, 0, 5);
+ };
+
+ let indexBuffer: null | GPUBuffer = null;
+ if (indexed) {
+ const kMaxIndices = 16 * 1024 * 1024;
+ indexBuffer = t.device.createBuffer({
+ size: kMaxIndices * Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(indexBuffer);
+ const indexData = new Uint32Array(indexBuffer.getMappedRange());
+ for (let i = 0; i < kMaxIndices; ++i) {
+ indexData[i] = i;
+ }
+ indexBuffer.unmap();
+ }
+
+ const colorAttachment = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 3, height: 3, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.VERTEX,
+ buffer: {},
+ },
+ ],
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: bgLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: paramsBuffer },
+ },
+ ],
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: t.device.createPipelineLayout({ bindGroupLayouts: [bgLayout] }),
+
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Params {
+ numVertices: u32,
+ numInstances: u32,
+ };
+
+ fn selectValue(index: u32, maxIndex: u32) -> f32 {
+ let highOrMid = select(0.0, 2.0 / 3.0, index == maxIndex - 1u);
+ return select(highOrMid, -2.0 / 3.0, index == 0u);
+ }
+
+ @group(0) @binding(0) var<uniform> params: Params;
+
+ @vertex fn main(
+ @builtin(vertex_index) v: u32,
+ @builtin(instance_index) i: u32)
+ -> @builtin(position) vec4<f32> {
+ let x = selectValue(v, params.numVertices);
+ let y = -selectValue(i, params.numInstances);
+ return vec4<f32>(x, y, 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+
+ const runPipeline = (numVertices: number, numInstances: number) => {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.0, g: 0.0, b: 1.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ if (indexBuffer !== null) {
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ }
+
+ if (indirect) {
+ writeIndirectParams(numVertices, numInstances);
+ if (indexed) {
+ pass.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ pass.drawIndirect(indirectBuffer, 0);
+ }
+ } else {
+ if (indexed) {
+ pass.drawIndexed(numVertices, numInstances);
+ } else {
+ pass.draw(numVertices, numInstances);
+ }
+ }
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { buffer: dst, bytesPerRow: kBytesPerRow },
+ { width: 3, height: 3, depthOrArrayLayers: 1 }
+ );
+
+ const params = new Uint32Array([numVertices, numInstances]);
+ t.device.queue.writeBuffer(paramsBuffer, 0, params, 0, 2);
+ t.device.queue.submit([encoder.finish()]);
+
+ const yellow = [0xff, 0xff, 0x00, 0xff];
+ const allYellow = new Uint8Array([...yellow, ...yellow, ...yellow]);
+ for (const row of [0, 1, 2]) {
+ t.expectGPUBufferValuesPassCheck(dst, data => checkElementsEqual(data, allYellow), {
+ srcByteOffset: row * 256,
+ type: Uint8Array,
+ typedLength: 12,
+ });
+ }
+ };
+
+ // If any iteration takes longer than this, we stop incrementing along that
+ // branch and move on to the next instance count. Note that the max
+ // supported vertex count for any iteration is 2**24 due to our choice of
+ // index buffer size.
+ const maxDurationMs = 100;
+ const counts = [
+ {
+ numInstances: 4,
+ vertexCounts: [2 ** 10, 2 ** 16, 2 ** 18, 2 ** 20, 2 ** 22, 2 ** 24],
+ },
+ {
+ numInstances: 2 ** 8,
+ vertexCounts: [2 ** 10, 2 ** 16, 2 ** 18, 2 ** 20, 2 ** 22],
+ },
+ {
+ numInstances: 2 ** 10,
+ vertexCounts: [2 ** 8, 2 ** 10, 2 ** 12, 2 ** 16, 2 ** 18, 2 ** 20],
+ },
+ {
+ numInstances: 2 ** 16,
+ vertexCounts: [2 ** 4, 2 ** 8, 2 ** 10, 2 ** 12, 2 ** 14],
+ },
+ {
+ numInstances: 2 ** 20,
+ vertexCounts: [2 ** 4, 2 ** 8, 2 ** 10],
+ },
+ ];
+ for (const { numInstances, vertexCounts } of counts) {
+ for (const numVertices of vertexCounts) {
+ const start = now();
+ runPipeline(numVertices, numInstances);
+ await t.device.queue.onSubmittedWorkDone();
+ const duration = now() - start;
+ if (duration >= maxDurationMs) {
+ break;
+ }
+ }
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts
new file mode 100644
index 0000000000..a0afeff2d0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/color_target_state.spec.ts
@@ -0,0 +1,890 @@
+export const description = `
+Test blending results.
+
+TODO:
+- Test result for all combinations of args (make sure each case is distinguishable from others
+- Test underflow/overflow has consistent behavior
+- ?
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, TypedArrayBufferView, unreachable } from '../../../../common/util/util.js';
+import {
+ kBlendFactors,
+ kBlendOperations,
+ kEncodableTextureFormats,
+ kTextureFormatInfo,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { float32ToFloat16Bits } from '../../../util/conversion.js';
+import { clamp } from '../../../util/math.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+class BlendingTest extends GPUTest {
+ createRenderPipelineForTest(colorTargetState: GPUColorTargetState): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ fragment: {
+ targets: [colorTargetState],
+ module: this.device.createShaderModule({
+ code: `
+ struct Params {
+ color : vec4<f32>
+ }
+ @group(0) @binding(0) var<uniform> params : Params;
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return params.color;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(3.0, -1.0),
+ vec2<f32>(-1.0, 3.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ }
+
+ createBindGroupForTest(layout: GPUBindGroupLayout, data: TypedArrayBufferView): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: this.makeBufferWithContents(data, GPUBufferUsage.UNIFORM),
+ },
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(BlendingTest);
+
+function mapColor(
+ col: GPUColorDict,
+ f: (v: number, k: keyof GPUColorDict) => number
+): GPUColorDict {
+ return {
+ r: f(col.r, 'r'),
+ g: f(col.g, 'g'),
+ b: f(col.b, 'b'),
+ a: f(col.a, 'a'),
+ };
+}
+
+function computeBlendFactor(
+ src: GPUColorDict,
+ dst: GPUColorDict,
+ blendColor: GPUColorDict | undefined,
+ factor: GPUBlendFactor
+): GPUColorDict {
+ switch (factor) {
+ case 'zero':
+ return { r: 0, g: 0, b: 0, a: 0 };
+ case 'one':
+ return { r: 1, g: 1, b: 1, a: 1 };
+ case 'src':
+ return { ...src };
+ case 'one-minus-src':
+ return mapColor(src, v => 1 - v);
+ case 'src-alpha':
+ return mapColor(src, () => src.a);
+ case 'one-minus-src-alpha':
+ return mapColor(src, () => 1 - src.a);
+ case 'dst':
+ return { ...dst };
+ case 'one-minus-dst':
+ return mapColor(dst, v => 1 - v);
+ case 'dst-alpha':
+ return mapColor(dst, () => dst.a);
+ case 'one-minus-dst-alpha':
+ return mapColor(dst, () => 1 - dst.a);
+ case 'src-alpha-saturated': {
+ const f = Math.min(src.a, 1 - dst.a);
+ return { r: f, g: f, b: f, a: 1 };
+ }
+ case 'constant':
+ assert(blendColor !== undefined);
+ return { ...blendColor };
+ case 'one-minus-constant':
+ assert(blendColor !== undefined);
+ return mapColor(blendColor, v => 1 - v);
+ default:
+ unreachable();
+ }
+}
+
+function computeBlendOperation(
+ src: GPUColorDict,
+ srcFactor: GPUColorDict,
+ dst: GPUColorDict,
+ dstFactor: GPUColorDict,
+ operation: GPUBlendOperation
+) {
+ switch (operation) {
+ case 'add':
+ return mapColor(src, (_, k) => srcFactor[k] * src[k] + dstFactor[k] * dst[k]);
+ case 'max':
+ return mapColor(src, (_, k) => Math.max(src[k], dst[k]));
+ case 'min':
+ return mapColor(src, (_, k) => Math.min(src[k], dst[k]));
+ case 'reverse-subtract':
+ return mapColor(src, (_, k) => dstFactor[k] * dst[k] - srcFactor[k] * src[k]);
+ case 'subtract':
+ return mapColor(src, (_, k) => srcFactor[k] * src[k] - dstFactor[k] * dst[k]);
+ }
+}
+
+g.test('blending,GPUBlendComponent')
+ .desc(
+ `Test all combinations of parameters for GPUBlendComponent.
+
+ Tests that parameters are correctly passed to the backend API and blend computations
+ are done correctly by blending a single pixel. The test uses rgba16float as the format
+ to avoid checking clamping behavior (tested in api,operation,rendering,blending:clamp,*).
+
+ Params:
+ - component= {color, alpha} - whether to test blending the color or the alpha component.
+ - srcFactor= {...all GPUBlendFactors}
+ - dstFactor= {...all GPUBlendFactors}
+ - operation= {...all GPUBlendOperations}`
+ )
+ .params(u =>
+ u //
+ .combine('component', ['color', 'alpha'] as const)
+ .combine('srcFactor', kBlendFactors)
+ .combine('dstFactor', kBlendFactors)
+ .combine('operation', kBlendOperations)
+ .filter(t => {
+ if (t.operation === 'min' || t.operation === 'max') {
+ return t.srcFactor === 'one' && t.dstFactor === 'one';
+ }
+ return true;
+ })
+ .beginSubcases()
+ .combine('srcColor', [{ r: 0.11, g: 0.61, b: 0.81, a: 0.44 }])
+ .combine('dstColor', [
+ { r: 0.51, g: 0.22, b: 0.71, a: 0.33 },
+ { r: 0.09, g: 0.73, b: 0.93, a: 0.81 },
+ ])
+ .expand('blendConstant', p => {
+ const needsBlendConstant =
+ p.srcFactor === 'one-minus-constant' ||
+ p.srcFactor === 'constant' ||
+ p.dstFactor === 'one-minus-constant' ||
+ p.dstFactor === 'constant';
+ return needsBlendConstant ? [{ r: 0.91, g: 0.82, b: 0.73, a: 0.64 }] : [undefined];
+ })
+ )
+ .fn(t => {
+ const textureFormat: GPUTextureFormat = 'rgba16float';
+ const srcColor = t.params.srcColor;
+ const dstColor = t.params.dstColor;
+ const blendConstant = t.params.blendConstant;
+
+ const srcFactor = computeBlendFactor(srcColor, dstColor, blendConstant, t.params.srcFactor);
+ const dstFactor = computeBlendFactor(srcColor, dstColor, blendConstant, t.params.dstFactor);
+
+ const expectedColor = computeBlendOperation(
+ srcColor,
+ srcFactor,
+ dstColor,
+ dstFactor,
+ t.params.operation
+ );
+
+ switch (t.params.component) {
+ case 'color':
+ expectedColor.a = srcColor.a;
+ break;
+ case 'alpha':
+ expectedColor.r = srcColor.r;
+ expectedColor.g = srcColor.g;
+ expectedColor.b = srcColor.b;
+ break;
+ }
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ fragment: {
+ targets: [
+ {
+ format: textureFormat,
+ blend: {
+ // Set both color/alpha to defaults...
+ color: {},
+ alpha: {},
+ // ... but then override the component we're testing.
+ [t.params.component]: {
+ srcFactor: t.params.srcFactor,
+ dstFactor: t.params.dstFactor,
+ operation: t.params.operation,
+ },
+ },
+ },
+ ],
+ module: t.device.createShaderModule({
+ code: `
+struct Uniform {
+ color: vec4<f32>
+};
+@group(0) @binding(0) var<uniform> u : Uniform;
+
+@fragment fn main() -> @location(0) vec4<f32> {
+ return u.color;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+@vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [1, 1, 1],
+ format: textureFormat,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: dstColor,
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(pipeline);
+ if (blendConstant) {
+ renderPass.setBlendConstant(blendConstant);
+ }
+ renderPass.setBindGroup(
+ 0,
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: t.makeBufferWithContents(
+ new Float32Array([srcColor.r, srcColor.g, srcColor.b, srcColor.a]),
+ GPUBufferUsage.UNIFORM
+ ),
+ },
+ },
+ ],
+ })
+ );
+ renderPass.draw(1);
+ renderPass.end();
+
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ const tolerance = 0.003;
+ const expectedLow = mapColor(expectedColor, v => v - tolerance);
+ const expectedHigh = mapColor(expectedColor, v => v + tolerance);
+
+ t.expectSinglePixelBetweenTwoValuesFloat16In2DTexture(
+ renderTarget,
+ textureFormat,
+ { x: 0, y: 0 },
+ {
+ exp: [
+ // Use Uint16Array to store Float16 value bits
+ new Uint16Array(
+ [expectedLow.r, expectedLow.g, expectedLow.b, expectedLow.a].map(float32ToFloat16Bits)
+ ),
+ new Uint16Array(
+ [expectedHigh.r, expectedHigh.g, expectedHigh.b, expectedHigh.a].map(
+ float32ToFloat16Bits
+ )
+ ),
+ ],
+ }
+ );
+ });
+
+const kBlendableFormats = kEncodableTextureFormats.filter(f => {
+ const info = kTextureFormatInfo[f];
+ return info.renderable && info.sampleType === 'float';
+});
+
+g.test('blending,formats')
+ .desc(
+ `Test blending results works for all formats that support it, and that blending is not applied
+ for formats that do not. Blending should be done in linear space for srgb formats.`
+ )
+ .params(u =>
+ u //
+ .combine('format', kBlendableFormats)
+ )
+ .fn(async t => {
+ const { format } = t.params;
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ fragment: {
+ targets: [
+ {
+ format,
+ blend: {
+ color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
+ alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
+ },
+ },
+ ],
+ module: t.device.createShaderModule({
+ code: `
+@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.4, 0.4, 0.4, 0.4);
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+@vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [1, 1, 1],
+ format,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: { r: 0.2, g: 0.2, b: 0.2, a: 0.2 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(pipeline);
+ renderPass.draw(1);
+ renderPass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ const expColor = { R: 0.6, G: 0.6, B: 0.6, A: 0.6 };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [1, 1, 1],
+ { expTexelView },
+ {
+ maxDiffULPsForNormFormat: 1,
+ maxDiffULPsForFloatFormat: 1,
+ }
+ );
+ t.expectOK(result);
+ });
+
+g.test('blend_constant,initial')
+ .desc(`Test that the blend constant is set to [0,0,0,0] at the beginning of a pass.`)
+ .fn(async t => {
+ const format = 'rgba8unorm';
+ const kSize = 1;
+ const kWhiteColorData = new Float32Array([255, 255, 255, 255]);
+
+ const blendComponent = { srcFactor: 'constant', dstFactor: 'one', operation: 'add' } as const;
+ const testPipeline = t.createRenderPipelineForTest({
+ format,
+ blend: { color: blendComponent, alpha: blendComponent },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [kSize, kSize],
+ format,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kWhiteColorData)
+ );
+ renderPass.draw(3);
+ // Draw [1,1,1,1] with `src * constant + dst * 1`.
+ // The blend constant defaults to [0,0,0,0], so the result is
+ // `[1,1,1,1] * [0,0,0,0] + [0,0,0,0] * 1` = [0,0,0,0].
+ renderPass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ // Check that the initial blend constant is black(0,0,0,0) after setting testPipeline which has
+ // a white color buffer data.
+ const expColor = { R: 0, G: 0, B: 0, A: 0 };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [kSize, kSize],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+g.test('blend_constant,setting')
+ .desc(`Test that setting the blend constant to the RGBA values works at the beginning of a pass.`)
+ .paramsSubcasesOnly([
+ { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ { r: 0.5, g: 1.0, b: 0.5, a: 0.0 },
+ { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ ])
+ .fn(async t => {
+ const { r, g, b, a } = t.params;
+
+ const format = 'rgba8unorm';
+ const kSize = 1;
+ const kWhiteColorData = new Float32Array([255, 255, 255, 255]);
+
+ const blendComponent = { srcFactor: 'constant', dstFactor: 'one', operation: 'add' } as const;
+ const testPipeline = t.createRenderPipelineForTest({
+ format,
+ blend: { color: blendComponent, alpha: blendComponent },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [kSize, kSize],
+ format,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBlendConstant({ r, g, b, a });
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kWhiteColorData)
+ );
+ renderPass.draw(3);
+ // Draw [1,1,1,1] with `src * constant + dst * 1`. The blend constant to [r,g,b,a], so the
+ // result is `[1,1,1,1] * [r,g,b,a] + [0,0,0,0] * 1` = [r,g,b,a].
+ renderPass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ // Check that the blend constant is the same as the given constant after setting the constant
+ // via setBlendConstant.
+ const expColor = { R: r, G: g, B: b, A: a };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [kSize, kSize],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+g.test('blend_constant,not_inherited')
+ .desc(`Test that the blending constant is not inherited between render passes.`)
+ .fn(async t => {
+ const format = 'rgba8unorm';
+ const kSize = 1;
+ const kWhiteColorData = new Float32Array([255, 255, 255, 255]);
+
+ const blendComponent = { srcFactor: 'constant', dstFactor: 'one', operation: 'add' } as const;
+ const testPipeline = t.createRenderPipelineForTest({
+ format,
+ blend: { color: blendComponent, alpha: blendComponent },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [kSize, kSize],
+ format,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBlendConstant({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 }); // Set to white color.
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kWhiteColorData)
+ );
+ renderPass.draw(3);
+ // Draw [1,1,1,1] with `src * constant + dst * 1`. The blend constant to [1,1,1,1], so the
+ // result is `[1,1,1,1] * [1,1,1,1] + [0,0,0,0] * 1` = [1,1,1,1].
+ renderPass.end();
+ }
+ {
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kWhiteColorData)
+ );
+ renderPass.draw(3);
+ // Draw [1,1,1,1] with `src * constant + dst * 1`. The blend constant defaults to [0,0,0,0],
+ // so the result is `[1,1,1,1] * [0,0,0,0] + [0,0,0,0] * 1` = [0,0,0,0].
+ renderPass.end();
+ }
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ // Check that the blend constant is not inherited from the first render pass.
+ const expColor = { R: 0, G: 0, B: 0, A: 0 };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [kSize, kSize],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+const kColorWriteCombinations: readonly GPUColorWriteFlags[] = [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+];
+
+g.test('color_write_mask,channel_work')
+ .desc(
+ `
+ Test that the color write mask works with the zero channel, a single channel, multiple channels,
+ and all channels.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('mask', kColorWriteCombinations)
+ )
+ .fn(async t => {
+ const { mask } = t.params;
+
+ const format = 'rgba8unorm';
+ const kSize = 1;
+
+ let r = 0,
+ g = 0,
+ b = 0,
+ a = 0;
+ if (mask & GPUConst.ColorWrite.RED) {
+ r = 1;
+ }
+ if (mask & GPUConst.ColorWrite.GREEN) {
+ g = 1;
+ }
+ if (mask & GPUConst.ColorWrite.BLUE) {
+ b = 1;
+ }
+ if (mask & GPUConst.ColorWrite.ALPHA) {
+ a = 1;
+ }
+
+ const testPipeline = t.createRenderPipelineForTest({
+ format,
+ writeMask: mask,
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [kSize, kSize],
+ format,
+ });
+
+ const kBaseColorData = new Float32Array([32, 64, 128, 192]);
+
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kBaseColorData)
+ );
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ const expColor = { R: r, G: g, B: b, A: a };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [kSize, kSize],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+g.test('color_write_mask,blending_disabled')
+ .desc(
+ `Test that the color write mask works when blending is disabled or set to the defaults
+ (which has the same blending result).`
+ )
+ .params(u => u.combine('disabled', [false, true]))
+ .fn(async t => {
+ const format = 'rgba8unorm';
+ const kSize = 1;
+
+ const blend = t.params.disabled ? undefined : { color: {}, alpha: {} };
+
+ const testPipeline = t.createRenderPipelineForTest({
+ format,
+ blend,
+ writeMask: GPUColorWrite.RED,
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [kSize, kSize],
+ format,
+ });
+
+ const kBaseColorData = new Float32Array([32, 64, 128, 192]);
+
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(testPipeline);
+ renderPass.setBindGroup(
+ 0,
+ t.createBindGroupForTest(testPipeline.getBindGroupLayout(0), kBaseColorData)
+ );
+ // Draw [1,1,1,1] with `src * 1 + dst * 0`. So the
+ // result is `[1,1,1,1] * [1,1,1,1] + [0,0,0,0] * 0` = [1,1,1,1].
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ const expColor = { R: 1, G: 0, B: 0, A: 0 };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [kSize, kSize],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+g.test('blending,clamping')
+ .desc(
+ `
+ Test that clamping occurs at the correct points in the blend process: src value, src factor, dst
+ factor, and output.
+ - TODO: Need to test snorm formats.
+ - TODO: Need to test src value, srcFactor and dstFactor.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', ['rgba8unorm', 'rg16float'] as const)
+ .combine('srcValue', [0.4, 0.6, 0.8, 1.0])
+ .combine('dstValue', [0.2, 0.4])
+ )
+ .fn(async t => {
+ const { format, srcValue, dstValue } = t.params;
+
+ const blendComponent = { srcFactor: 'one', dstFactor: 'one', operation: 'add' } as const;
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ fragment: {
+ targets: [
+ {
+ format,
+ blend: {
+ color: blendComponent,
+ alpha: blendComponent,
+ },
+ },
+ ],
+ module: t.device.createShaderModule({
+ code: `
+@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(${srcValue}, ${srcValue}, ${srcValue}, ${srcValue});
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+@vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const renderTarget = t.device.createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ size: [1, 1, 1],
+ format,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: { r: dstValue, g: dstValue, b: dstValue, a: dstValue },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(pipeline);
+ renderPass.draw(1);
+ renderPass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ let expValue: number;
+ switch (format) {
+ case 'rgba8unorm': // unorm types should clamp if the sum of srcValue and dstValue exceeds 1.
+ expValue = clamp(srcValue + dstValue, { min: 0, max: 1 });
+ break;
+ case 'rg16float': // float format types doesn't clamp.
+ expValue = srcValue + dstValue;
+ break;
+ }
+
+ const expColor = { R: expValue, G: expValue, B: expValue, A: expValue };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: renderTarget },
+ [1, 1, 1],
+ { expTexelView },
+ {
+ maxDiffULPsForNormFormat: 1,
+ maxDiffULPsForFloatFormat: 1,
+ }
+ );
+ t.expectOK(result);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts
new file mode 100644
index 0000000000..853de3ee6a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth.spec.ts
@@ -0,0 +1,549 @@
+export const description = `
+Test related to depth buffer, depth op, compare func, etc.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { TypedArrayBufferView } from '../../../../common/util/util.js';
+import { kDepthStencilFormats, kTextureFormatInfo } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+const backgroundColor = [0x00, 0x00, 0x00, 0xff];
+const triangleColor = [0xff, 0xff, 0xff, 0xff];
+
+const kBaseColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
+const kRedStencilColor = new Float32Array([1.0, 0.0, 0.0, 1.0]);
+const kGreenStencilColor = new Float32Array([0.0, 1.0, 0.0, 1.0]);
+
+type TestStates = {
+ state: GPUDepthStencilState;
+ color: Float32Array;
+ depth: number;
+};
+
+class DepthTest extends GPUTest {
+ runDepthStateTest(testStates: TestStates[], expectedColor: Float32Array) {
+ const renderTargetFormat = 'rgba8unorm';
+
+ const renderTarget = this.device.createTexture({
+ format: renderTargetFormat,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const depthStencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
+ const depthTexture = this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: depthStencilFormat,
+ sampleCount: 1,
+ mipLevelCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
+ });
+
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthTexture.createView(),
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'store',
+ };
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ loadOp: 'load',
+ },
+ ],
+ depthStencilAttachment,
+ });
+
+ // Draw a triangle with the given depth state, color, and depth.
+ for (const test of testStates) {
+ const testPipeline = this.createRenderPipelineForTest(test.state, test.depth);
+ pass.setPipeline(testPipeline);
+ pass.setBindGroup(
+ 0,
+ this.createBindGroupForTest(testPipeline.getBindGroupLayout(0), test.color)
+ );
+ pass.draw(1);
+ }
+
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ const expColor = {
+ R: expectedColor[0],
+ G: expectedColor[1],
+ B: expectedColor[2],
+ A: expectedColor[3],
+ };
+ const expTexelView = TexelView.fromTexelsAsColors(renderTargetFormat, coords => expColor);
+
+ const result = textureContentIsOKByT2B(
+ this,
+ { texture: renderTarget },
+ [1, 1],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ this.eventualExpectOK(result);
+ this.trackForCleanup(renderTarget);
+ }
+
+ createRenderPipelineForTest(
+ depthStencil: GPUDepthStencilState,
+ depth: number
+ ): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, ${depth}, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module: this.device.createShaderModule({
+ code: `
+ struct Params {
+ color : vec4<f32>
+ }
+ @group(0) @binding(0) var<uniform> params : Params;
+
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(params.color);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ primitive: { topology: 'point-list' },
+ depthStencil,
+ });
+ }
+
+ createBindGroupForTest(layout: GPUBindGroupLayout, data: TypedArrayBufferView): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: this.makeBufferWithContents(data, GPUBufferUsage.UNIFORM),
+ },
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(DepthTest);
+
+g.test('depth_disabled')
+ .desc('Tests render results with depth test disabled.')
+ .fn(async t => {
+ const depthSpencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
+ const state = { format: depthSpencilFormat };
+
+ const testStates = [
+ { state, color: kBaseColor, depth: 0.0 },
+ { state, color: kRedStencilColor, depth: 0.5 },
+ { state, color: kGreenStencilColor, depth: 1.0 },
+ ];
+
+ // Test that for all combinations and ensure the last triangle drawn is the one visible
+ // regardless of depth testing.
+ for (let last = 0; last < 3; ++last) {
+ const i = (last + 1) % 3;
+ const j = (last + 2) % 3;
+
+ t.runDepthStateTest([testStates[i], testStates[j], testStates[last]], testStates[last].color);
+ t.runDepthStateTest([testStates[j], testStates[i], testStates[last]], testStates[last].color);
+ }
+ });
+
+g.test('depth_write_disabled')
+ .desc(
+ `
+ Test that depthWriteEnabled behaves as expected.
+ If enabled, a depth value of 0.0 is written.
+ If disabled, it's not written, so it keeps the previous value of 1.0.
+ Use a depthCompare: 'equal' check at the end to check the value.
+ `
+ )
+ .params(u =>
+ u //
+ .combineWithParams([
+ { depthWriteEnabled: false, lastDepth: 0.0, _expectedColor: kRedStencilColor },
+ { depthWriteEnabled: true, lastDepth: 0.0, _expectedColor: kGreenStencilColor },
+ { depthWriteEnabled: false, lastDepth: 1.0, _expectedColor: kGreenStencilColor },
+ { depthWriteEnabled: true, lastDepth: 1.0, _expectedColor: kRedStencilColor },
+ ])
+ )
+ .fn(async t => {
+ const { depthWriteEnabled, lastDepth, _expectedColor } = t.params;
+
+ const depthSpencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
+
+ const stencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ depthFailOp: 'keep',
+ passOp: 'keep',
+ } as const;
+
+ const baseState = {
+ format: depthSpencilFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'always',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ stencilReadMask: 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const depthWriteState = {
+ format: depthSpencilFormat,
+ depthWriteEnabled,
+ depthCompare: 'always',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ stencilReadMask: 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const checkState = {
+ format: depthSpencilFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'equal',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ stencilReadMask: 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const testStates = [
+ // Draw a base point with depth write enabled.
+ { state: baseState, color: kBaseColor, depth: 1.0 },
+ // Draw a second point without depth write enabled.
+ { state: depthWriteState, color: kRedStencilColor, depth: 0.0 },
+ // Draw a third point which should occlude the second even though it is behind it.
+ { state: checkState, color: kGreenStencilColor, depth: lastDepth },
+ ];
+
+ t.runDepthStateTest(testStates, _expectedColor);
+ });
+
+g.test('depth_test_fail')
+ .desc(
+ `
+ Test that render results on depth test failure cases with 'less' depthCompare operation and
+ depthWriteEnabled is true.
+ `
+ )
+ .params(u =>
+ u //
+ .combineWithParams([
+ { secondDepth: 1.0, lastDepth: 2.0, _expectedColor: kBaseColor }, // fail -> fail.
+ { secondDepth: 0.0, lastDepth: 2.0, _expectedColor: kRedStencilColor }, // pass -> fail.
+ { secondDepth: 2.0, lastDepth: 0.9, _expectedColor: kGreenStencilColor }, // fail -> pass.
+ ] as const)
+ )
+ .fn(async t => {
+ const { secondDepth, lastDepth, _expectedColor } = t.params;
+
+ const depthSpencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
+
+ const baseState = {
+ format: depthSpencilFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'always',
+ stencilReadMask: 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const depthTestState = {
+ format: depthSpencilFormat,
+ depthWriteEnabled: true,
+ depthCompare: 'less',
+ stencilReadMask: 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const testStates = [
+ { state: baseState, color: kBaseColor, depth: 1.0 },
+ { state: depthTestState, color: kRedStencilColor, depth: secondDepth },
+ { state: depthTestState, color: kGreenStencilColor, depth: lastDepth },
+ ];
+
+ t.runDepthStateTest(testStates, _expectedColor);
+ });
+
+// Use a depth value that's not exactly 0.5 because it is exactly between two depth16unorm value and
+// can get rounded either way (and a different way between shaders and clearDepthValue).
+const kMiddleDepthValue = 0.5001;
+
+g.test('depth_compare_func')
+ .desc(
+ `Tests each depth compare function works properly. Clears the depth attachment to various values, and renders a point at depth 0.5 with various depthCompare modes.`
+ )
+ .params(u =>
+ u
+ .combine(
+ 'format',
+ kDepthStencilFormats.filter(format => kTextureFormatInfo[format].depth)
+ )
+ .combineWithParams([
+ { depthCompare: 'never', depthClearValue: 1.0, _expected: backgroundColor },
+ { depthCompare: 'never', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
+ { depthCompare: 'never', depthClearValue: 0.0, _expected: backgroundColor },
+ { depthCompare: 'less', depthClearValue: 1.0, _expected: triangleColor },
+ { depthCompare: 'less', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
+ { depthCompare: 'less', depthClearValue: 0.0, _expected: backgroundColor },
+ { depthCompare: 'less-equal', depthClearValue: 1.0, _expected: triangleColor },
+ {
+ depthCompare: 'less-equal',
+ depthClearValue: kMiddleDepthValue,
+ _expected: triangleColor,
+ },
+ { depthCompare: 'less-equal', depthClearValue: 0.0, _expected: backgroundColor },
+ { depthCompare: 'equal', depthClearValue: 1.0, _expected: backgroundColor },
+ { depthCompare: 'equal', depthClearValue: kMiddleDepthValue, _expected: triangleColor },
+ { depthCompare: 'equal', depthClearValue: 0.0, _expected: backgroundColor },
+ { depthCompare: 'not-equal', depthClearValue: 1.0, _expected: triangleColor },
+ {
+ depthCompare: 'not-equal',
+ depthClearValue: kMiddleDepthValue,
+ _expected: backgroundColor,
+ },
+ { depthCompare: 'not-equal', depthClearValue: 0.0, _expected: triangleColor },
+ { depthCompare: 'greater-equal', depthClearValue: 1.0, _expected: backgroundColor },
+ {
+ depthCompare: 'greater-equal',
+ depthClearValue: kMiddleDepthValue,
+ _expected: triangleColor,
+ },
+ { depthCompare: 'greater-equal', depthClearValue: 0.0, _expected: triangleColor },
+ { depthCompare: 'greater', depthClearValue: 1.0, _expected: backgroundColor },
+ { depthCompare: 'greater', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
+ { depthCompare: 'greater', depthClearValue: 0.0, _expected: triangleColor },
+ { depthCompare: 'always', depthClearValue: 1.0, _expected: triangleColor },
+ { depthCompare: 'always', depthClearValue: kMiddleDepthValue, _expected: triangleColor },
+ { depthCompare: 'always', depthClearValue: 0.0, _expected: triangleColor },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { depthCompare, depthClearValue, _expected, format } = t.params;
+
+ const colorAttachmentFormat = 'rgba8unorm';
+ const colorAttachment = t.device.createTexture({
+ format: colorAttachmentFormat,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const depthTexture = t.device.createTexture({
+ size: { width: 1, height: 1 },
+ format,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ });
+ const depthTextureView = depthTexture.createView();
+
+ const pipelineDescriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.5, 0.5, ${kMiddleDepthValue}, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorAttachmentFormat }],
+ },
+ primitive: { topology: 'point-list' },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare,
+ format,
+ },
+ };
+ const pipeline = t.device.createRenderPipeline(pipelineDescriptor);
+
+ const encoder = t.device.createCommandEncoder();
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthTextureView,
+ depthClearValue,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ };
+ if (kTextureFormatInfo[format].stencil) {
+ depthStencilAttachment.stencilClearValue = 0;
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ depthStencilAttachment,
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ colorAttachmentFormat,
+ { x: 0, y: 0 },
+ { exp: new Uint8Array(_expected) }
+ );
+ });
+
+g.test('reverse_depth')
+ .desc(
+ `Tests simple rendering with reversed depth buffer, ensures depth test works properly: fragments are in correct order and out of range fragments are clipped.
+ Note that in real use case the depth range remapping is done by the modified projection matrix.
+(see https://developer.nvidia.com/content/depth-precision-visualized).`
+ )
+ .params(u => u.combine('reversed', [false, true]))
+ .fn(async t => {
+ const colorAttachmentFormat = 'rgba8unorm';
+ const colorAttachment = t.device.createTexture({
+ format: colorAttachmentFormat,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const depthBufferFormat = 'depth32float';
+ const depthTexture = t.device.createTexture({
+ size: { width: 1, height: 1 },
+ format: depthBufferFormat,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ });
+ const depthTextureView = depthTexture.createView();
+
+ const pipelineDescriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ struct Output {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) color : vec4<f32>,
+ };
+
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32,
+ @builtin(instance_index) InstanceIndex : u32) -> Output {
+ // TODO: remove workaround for Tint unary array access broke
+ var zv : array<vec2<f32>, 4> = array<vec2<f32>, 4>(
+ vec2<f32>(0.2, 0.2),
+ vec2<f32>(0.3, 0.3),
+ vec2<f32>(-0.1, -0.1),
+ vec2<f32>(1.1, 1.1));
+ let z : f32 = zv[InstanceIndex].x;
+
+ var output : Output;
+ output.Position = vec4<f32>(0.5, 0.5, z, 1.0);
+ var colors : array<vec4<f32>, 4> = array<vec4<f32>, 4>(
+ vec4<f32>(1.0, 0.0, 0.0, 1.0),
+ vec4<f32>(0.0, 1.0, 0.0, 1.0),
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
+ );
+ output.color = colors[InstanceIndex];
+ return output;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment fn main(
+ @location(0) color : vec4<f32>
+ ) -> @location(0) vec4<f32> {
+ return color;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorAttachmentFormat }],
+ },
+ primitive: { topology: 'point-list' },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare: t.params.reversed ? 'greater' : 'less',
+ format: depthBufferFormat,
+ },
+ };
+ const pipeline = t.device.createRenderPipeline(pipelineDescriptor);
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ depthStencilAttachment: {
+ view: depthTextureView,
+
+ depthClearValue: t.params.reversed ? 0.0 : 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ },
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(1, 4);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ colorAttachmentFormat,
+ { x: 0, y: 0 },
+ {
+ exp: new Uint8Array(
+ t.params.reversed ? [0x00, 0xff, 0x00, 0xff] : [0xff, 0x00, 0x00, 0xff]
+ ),
+ }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts
new file mode 100644
index 0000000000..01e2dc904d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_bias.spec.ts
@@ -0,0 +1,369 @@
+export const description = `
+Tests render results with different depth bias values like 'positive', 'negative',
+'slope', 'clamp', etc.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../common/util/util.js';
+import {
+ DepthStencilFormat,
+ EncodableTextureFormat,
+ kTextureFormatInfo,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+enum QuadAngle {
+ Flat,
+ TiltedX,
+}
+
+// Floating point depth buffers use the following formula to calculate bias
+// bias = depthBias * 2 ** (exponent(max z of primitive) - number of bits in mantissa) +
+// slopeScale * maxSlope
+// https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias
+// https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdSetDepthBias.html
+// https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1516269-setdepthbias
+//
+// To get a final bias of 0.25 for primitives with z = 0.25, we can use
+// depthBias = 0.25 / (2 ** (-2 - 23)) = 8388608.
+const kPointTwoFiveBiasForPointTwoFiveZOnFloat = 8388608;
+
+class DepthBiasTest extends GPUTest {
+ runDepthBiasTestInternal(
+ depthFormat: DepthStencilFormat,
+ {
+ quadAngle,
+ bias,
+ biasSlopeScale,
+ biasClamp,
+ initialDepth,
+ }: {
+ quadAngle: QuadAngle;
+ bias: number;
+ biasSlopeScale: number;
+ biasClamp: number;
+ initialDepth: number;
+ }
+ ): { renderTarget: GPUTexture; depthTexture: GPUTexture } {
+ const renderTargetFormat = 'rgba8unorm';
+ const depthFormatInfo = kTextureFormatInfo[depthFormat];
+
+ let vertexShaderCode: string;
+ switch (quadAngle) {
+ case QuadAngle.Flat:
+ // Draw a square at z = 0.25.
+ vertexShaderCode = `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>( 1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.25, 1.0);
+ }
+ `;
+ break;
+ case QuadAngle.TiltedX:
+ // Draw a square ranging from 0 to 0.5, bottom to top.
+ vertexShaderCode = `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec3<f32>, 6>(
+ vec3<f32>(-1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, -1.0, 0.0),
+ vec3<f32>(-1.0, 1.0, 0.5),
+ vec3<f32>(-1.0, 1.0, 0.5),
+ vec3<f32>( 1.0, -1.0, 0.0),
+ vec3<f32>( 1.0, 1.0, 0.5));
+ return vec4<f32>(pos[VertexIndex], 1.0);
+ }
+ `;
+ break;
+ default:
+ unreachable();
+ }
+
+ const renderTarget = this.device.createTexture({
+ format: renderTargetFormat,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const depthTexture = this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: depthFormat,
+ sampleCount: 1,
+ mipLevelCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthTexture.createView(),
+ depthLoadOp: depthFormatInfo.depth ? 'clear' : undefined,
+ depthStoreOp: depthFormatInfo.depth ? 'store' : undefined,
+ stencilLoadOp: depthFormatInfo.stencil ? 'clear' : undefined,
+ stencilStoreOp: depthFormatInfo.stencil ? 'store' : undefined,
+ depthClearValue: initialDepth,
+ };
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ loadOp: 'load',
+ },
+ ],
+ depthStencilAttachment,
+ });
+
+ let depthCompare: GPUCompareFunction = 'always';
+ if (depthFormat !== 'depth32float') {
+ depthCompare = 'greater';
+ }
+
+ const testState = {
+ format: depthFormat,
+ depthCompare,
+ depthWriteEnabled: true,
+ depthBias: bias,
+ depthBiasSlopeScale: biasSlopeScale,
+ depthBiasClamp: biasClamp,
+ } as const;
+
+ // Draw a square with the given depth state and bias values.
+ const testPipeline = this.createRenderPipelineForTest(vertexShaderCode, testState);
+ pass.setPipeline(testPipeline);
+ pass.draw(6);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ return { renderTarget, depthTexture };
+ }
+
+ runDepthBiasTest(
+ depthFormat: EncodableTextureFormat & DepthStencilFormat,
+ {
+ quadAngle,
+ bias,
+ biasSlopeScale,
+ biasClamp,
+ _expectedDepth,
+ }: {
+ quadAngle: QuadAngle;
+ bias: number;
+ biasSlopeScale: number;
+ biasClamp: number;
+ _expectedDepth: number;
+ }
+ ) {
+ const { renderTarget, depthTexture } = this.runDepthBiasTestInternal(depthFormat, {
+ quadAngle,
+ bias,
+ biasSlopeScale,
+ biasClamp,
+ initialDepth: 0,
+ });
+
+ const expColor = { Depth: _expectedDepth };
+ const expTexelView = TexelView.fromTexelsAsColors(depthFormat, coords => expColor);
+
+ const result = textureContentIsOKByT2B(
+ this,
+ { texture: depthTexture },
+ [1, 1],
+ { expTexelView },
+ { maxDiffULPsForFloatFormat: 1 }
+ );
+ this.eventualExpectOK(result);
+ this.trackForCleanup(renderTarget);
+ this.trackForCleanup(depthTexture);
+ }
+
+ runDepthBiasTestFor24BitFormat(
+ depthFormat: DepthStencilFormat,
+ {
+ quadAngle,
+ bias,
+ biasSlopeScale,
+ biasClamp,
+ _expectedColor,
+ }: {
+ quadAngle: QuadAngle;
+ bias: number;
+ biasSlopeScale: number;
+ biasClamp: number;
+ _expectedColor: Float32Array;
+ }
+ ) {
+ const { renderTarget, depthTexture } = this.runDepthBiasTestInternal(depthFormat, {
+ quadAngle,
+ bias,
+ biasSlopeScale,
+ biasClamp,
+ initialDepth: 0.4,
+ });
+
+ const renderTargetFormat = 'rgba8unorm';
+ const expColor = {
+ R: _expectedColor[0],
+ G: _expectedColor[1],
+ B: _expectedColor[2],
+ A: _expectedColor[3],
+ };
+ const expTexelView = TexelView.fromTexelsAsColors(renderTargetFormat, coords => expColor);
+
+ const result = textureContentIsOKByT2B(
+ this,
+ { texture: renderTarget },
+ [1, 1],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ this.eventualExpectOK(result);
+ this.trackForCleanup(renderTarget);
+ this.trackForCleanup(depthTexture);
+ }
+
+ createRenderPipelineForTest(
+ vertex: string,
+ depthStencil: GPUDepthStencilState
+ ): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: vertex,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ depthStencil,
+ });
+ }
+}
+
+export const g = makeTestGroup(DepthBiasTest);
+
+g.test('depth_bias')
+ .desc(
+ `
+ Tests that a square with different depth bias values like 'positive', 'negative',
+ 'slope', 'clamp', etc. is drawn as expected.
+ `
+ )
+ .params(u =>
+ u //
+ .combineWithParams([
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+ biasSlopeScale: 0,
+ biasClamp: 0,
+ _expectedDepth: 0.5,
+ },
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+ biasSlopeScale: 0,
+ biasClamp: 0.125,
+ _expectedDepth: 0.375,
+ },
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: -kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+ biasSlopeScale: 0,
+ biasClamp: 0.125,
+ _expectedDepth: 0,
+ },
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: -kPointTwoFiveBiasForPointTwoFiveZOnFloat,
+ biasSlopeScale: 0,
+ biasClamp: -0.125,
+ _expectedDepth: 0.125,
+ },
+ {
+ quadAngle: QuadAngle.TiltedX,
+ bias: 0,
+ biasSlopeScale: 0,
+ biasClamp: 0,
+ _expectedDepth: 0.25,
+ },
+ {
+ quadAngle: QuadAngle.TiltedX,
+ bias: 0,
+ biasSlopeScale: 1,
+ biasClamp: 0,
+ _expectedDepth: 0.75,
+ },
+ {
+ quadAngle: QuadAngle.TiltedX,
+ bias: 0,
+ biasSlopeScale: -0.5,
+ biasClamp: 0,
+ _expectedDepth: 0,
+ },
+ ] as const)
+ )
+ .fn(async t => {
+ t.runDepthBiasTest('depth32float', t.params);
+ });
+
+g.test('depth_bias_24bit_format')
+ .desc(
+ `
+ Tests that a square with different depth bias values like 'positive', 'negative',
+ 'slope', 'clamp', etc. is drawn as expected with 24 bit depth format.
+
+ TODO: Enhance these tests by reading back the depth (emulating the copy using texture sampling)
+ and checking the result directly, like the non-24-bit depth tests, instead of just relying on
+ whether the depth test passes or fails.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', ['depth24plus', 'depth24plus-stencil8'] as const)
+ .combineWithParams([
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: 0.25 * (1 << 25),
+ biasSlopeScale: 0,
+ biasClamp: 0,
+ _expectedColor: new Float32Array([1.0, 0.0, 0.0, 1.0]),
+ },
+ {
+ quadAngle: QuadAngle.TiltedX,
+ bias: 0.25 * (1 << 25),
+ biasSlopeScale: 1,
+ biasClamp: 0,
+ _expectedColor: new Float32Array([1.0, 0.0, 0.0, 1.0]),
+ },
+ {
+ quadAngle: QuadAngle.Flat,
+ bias: 0.25 * (1 << 25),
+ biasSlopeScale: 0,
+ biasClamp: 0.1,
+ _expectedColor: new Float32Array([0.0, 0.0, 0.0, 0.0]),
+ },
+ ] as const)
+ )
+ .fn(async t => {
+ const { format } = t.params;
+ t.runDepthBiasTestFor24BitFormat(format, t.params);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts
new file mode 100644
index 0000000000..00a474d4bb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/depth_clip_clamp.spec.ts
@@ -0,0 +1,524 @@
+export const description = `
+Tests for depth clipping, depth clamping (at various points in the pipeline), and maybe extended
+depth ranges as well.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kDepthStencilFormats, kTextureFormatInfo } from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import {
+ checkElementsBetween,
+ checkElementsPassPredicate,
+ CheckElementsSupplementalTableRows,
+} from '../../../util/check_contents.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('depth_clamp_and_clip')
+ .desc(
+ `
+Depth written to the depth attachment should always be in the range of the viewport depth,
+even if it was written by the fragment shader (using frag_depth). If depth clipping is enabled,
+primitives should be clipped to the viewport depth before rasterization; if not, these fragments
+should be rasterized, and the fragment shader should receive out-of-viewport position.z values.
+
+To test this, render NxN points, with N vertex depth values, by (if writeDepth=true) N
+frag_depth values with the viewport depth set to [0.25,0.75].
+
+While rendering, check the fragment input position.z has the expected value (for all fragments that
+were produced by the rasterizer) by writing the diff to a storage buffer, which is later checked to
+be all (near) 0.
+
+Then, run another pass (which outputs every point at z=0.5 to avoid clipping) to verify the depth
+buffer contents by outputting the expected depth with depthCompare:'not-equal': any fragments that
+have unexpected values then get drawn to the color buffer, which is later checked to be empty.`
+ )
+ .params(u =>
+ u //
+ .combine('format', kDepthStencilFormats)
+ .filter(p => kTextureFormatInfo[p.format].depth)
+ .combine('unclippedDepth', [undefined, false, true])
+ .combine('writeDepth', [false, true])
+ .combine('multisampled', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+
+ t.selectDeviceOrSkipTestCase([
+ t.params.unclippedDepth ? 'depth-clip-control' : undefined,
+ info.feature,
+ ]);
+ })
+ .fn(async t => {
+ const { format, unclippedDepth, writeDepth, multisampled } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ /** Number of depth values to test for both vertex output and frag_depth output. */
+ const kNumDepthValues = 8;
+ /** Test every combination of vertex output and frag_depth output. */
+ const kNumTestPoints = kNumDepthValues * kNumDepthValues;
+ const kViewportMinDepth = 0.25;
+ const kViewportMaxDepth = 0.75;
+
+ const shaderSource = `
+ // Test depths, with viewport range corresponding to [0,1].
+ var<private> kDepths: array<f32, ${kNumDepthValues}> = array<f32, ${kNumDepthValues}>(
+ -1.0, -0.5, 0.0, 0.25, 0.75, 1.0, 1.5, 2.0);
+
+ const vpMin: f32 = ${kViewportMinDepth};
+ const vpMax: f32 = ${kViewportMaxDepth};
+
+ // Draw the points in a straight horizontal row, one per pixel.
+ fn vertexX(idx: u32) -> f32 {
+ return (f32(idx) + 0.5) * 2.0 / ${kNumTestPoints}.0 - 1.0;
+ }
+
+ // Test vertex shader's position.z output.
+ // Here, the viewport range corresponds to position.z in [0,1].
+ fn vertexZ(idx: u32) -> f32 {
+ return kDepths[idx / ${kNumDepthValues}u];
+ }
+
+ // Test fragment shader's expected position.z input.
+ // Here, the viewport range corresponds to position.z in [vpMin,vpMax], but
+ // unclipped values extend beyond that range.
+ fn expectedFragPosZ(idx: u32) -> f32 {
+ return vpMin + vertexZ(idx) * (vpMax - vpMin);
+ }
+
+ //////// "Test" entry points
+
+ struct VFTest {
+ @builtin(position) pos: vec4<f32>,
+ @location(0) @interpolate(flat) vertexIndex: u32,
+ };
+
+ @vertex
+ fn vtest(@builtin(vertex_index) idx: u32) -> VFTest {
+ var vf: VFTest;
+ vf.pos = vec4<f32>(vertexX(idx), 0.0, vertexZ(idx), 1.0);
+ vf.vertexIndex = idx;
+ return vf;
+ }
+
+ struct Output {
+ // Each fragment (that didn't get clipped) writes into one element of this output.
+ // (Anything that doesn't get written is already zero.)
+ fragInputZDiff: array<f32, ${kNumTestPoints}>
+ };
+ @group(0) @binding(0) var <storage, read_write> output: Output;
+
+ fn checkZ(vf: VFTest) {
+ output.fragInputZDiff[vf.vertexIndex] = vf.pos.z - expectedFragPosZ(vf.vertexIndex);
+ }
+
+ @fragment
+ fn ftest_WriteDepth(vf: VFTest) -> @builtin(frag_depth) f32 {
+ checkZ(vf);
+ return kDepths[vf.vertexIndex % ${kNumDepthValues}u];
+ }
+
+ @fragment
+ fn ftest_NoWriteDepth(vf: VFTest) {
+ checkZ(vf);
+ }
+
+ //////// "Check" entry points
+
+ struct VFCheck {
+ @builtin(position) pos: vec4<f32>,
+ @location(0) @interpolate(flat) vertexIndex: u32,
+ };
+
+ @vertex
+ fn vcheck(@builtin(vertex_index) idx: u32) -> VFCheck {
+ var vf: VFCheck;
+ // Depth=0.5 because we want to render every point, not get clipped.
+ vf.pos = vec4<f32>(vertexX(idx), 0.0, 0.5, 1.0);
+ vf.vertexIndex = idx;
+ return vf;
+ }
+
+ struct FCheck {
+ @builtin(frag_depth) depth: f32,
+ @location(0) color: f32,
+ };
+
+ @fragment
+ fn fcheck(vf: VFCheck) -> FCheck {
+ let vertZ = vertexZ(vf.vertexIndex);
+ let outOfRange = vertZ < 0.0 || vertZ > 1.0;
+ let expFragPosZ = expectedFragPosZ(vf.vertexIndex);
+
+ let writtenDepth = kDepths[vf.vertexIndex % ${kNumDepthValues}u];
+
+ let expectedDepthWriteInput = ${writeDepth ? 'writtenDepth' : 'expFragPosZ'};
+ var expectedDepthBufferValue = clamp(expectedDepthWriteInput, vpMin, vpMax);
+ if (${!unclippedDepth} && outOfRange) {
+ // Test fragment should have been clipped; expect the depth attachment to
+ // have its clear value (0.5).
+ expectedDepthBufferValue = 0.5;
+ }
+
+ var f: FCheck;
+ f.depth = expectedDepthBufferValue;
+ f.color = 1.0; // Color written if the resulting depth is unexpected.
+ return f;
+ }
+ `;
+ const module = t.device.createShaderModule({ code: shaderSource });
+
+ // Draw points at different vertex depths and fragment depths into the depth attachment,
+ // with a viewport of [0.25,0.75].
+ const testPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vtest' },
+ primitive: {
+ topology: 'point-list',
+ unclippedDepth,
+ },
+ depthStencil: { format, depthWriteEnabled: true },
+ multisample: multisampled ? { count: 4 } : undefined,
+ fragment: {
+ module,
+ entryPoint: writeDepth ? 'ftest_WriteDepth' : 'ftest_NoWriteDepth',
+ targets: [],
+ },
+ });
+
+ // Use depth comparison to check that the depth attachment now has the expected values.
+ const checkPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vcheck' },
+ primitive: { topology: 'point-list' },
+ depthStencil: {
+ format,
+ // NOTE: This check is probably very susceptible to floating point error. If it fails, maybe
+ // replace it with two checks (less + greater) with an epsilon applied in the check shader?
+ depthCompare: 'not-equal', // Expect every depth value to be exactly equal.
+ depthWriteEnabled: true, // If the check failed, overwrite with the expected result.
+ },
+ multisample: multisampled ? { count: 4 } : undefined,
+ fragment: { module, entryPoint: 'fcheck', targets: [{ format: 'r8unorm' }] },
+ });
+
+ const dsTexture = t.device.createTexture({
+ format,
+ size: [kNumTestPoints],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ sampleCount: multisampled ? 4 : 1,
+ });
+ const dsTextureView = dsTexture.createView();
+
+ const checkTextureDesc = {
+ format: 'r8unorm' as const,
+ size: [kNumTestPoints],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ };
+ const checkTexture = t.device.createTexture(checkTextureDesc);
+ const checkTextureView = checkTexture.createView();
+ const checkTextureMSView = multisampled
+ ? t.device.createTexture({ ...checkTextureDesc, sampleCount: 4 }).createView()
+ : undefined;
+
+ const dsActual =
+ !multisampled && info.bytesPerBlock
+ ? t.device.createBuffer({
+ size: kNumTestPoints * info.bytesPerBlock,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ })
+ : undefined;
+ const dsExpected =
+ !multisampled && info.bytesPerBlock
+ ? t.device.createBuffer({
+ size: kNumTestPoints * info.bytesPerBlock,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ })
+ : undefined;
+ const checkBuffer = t.device.createBuffer({
+ size: kNumTestPoints,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ });
+
+ const fragInputZFailedBuffer = t.device.createBuffer({
+ size: 4 * kNumTestPoints,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ const testBindGroup = t.device.createBindGroup({
+ layout: testPipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: fragInputZFailedBuffer } }],
+ });
+
+ const enc = t.device.createCommandEncoder();
+ {
+ const pass = enc.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: dsTextureView,
+ depthClearValue: 0.5, // Will see this depth value if the fragment was clipped.
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: info.stencil ? 0 : undefined,
+ stencilLoadOp: info.stencil ? 'clear' : undefined,
+ stencilStoreOp: info.stencil ? 'discard' : undefined,
+ },
+ });
+ pass.setPipeline(testPipeline);
+ pass.setBindGroup(0, testBindGroup);
+ pass.setViewport(0, 0, kNumTestPoints, 1, kViewportMinDepth, kViewportMaxDepth);
+ pass.draw(kNumTestPoints);
+ pass.end();
+ }
+ if (dsActual) {
+ enc.copyTextureToBuffer({ texture: dsTexture }, { buffer: dsActual }, [kNumTestPoints]);
+ }
+ {
+ const clearValue = [0, 0, 0, 0]; // Will see this color if the check passed.
+ const pass = enc.beginRenderPass({
+ colorAttachments: [
+ checkTextureMSView
+ ? {
+ view: checkTextureMSView,
+ resolveTarget: checkTextureView,
+ clearValue,
+ loadOp: 'clear',
+ storeOp: 'discard',
+ }
+ : { view: checkTextureView, clearValue, loadOp: 'clear', storeOp: 'store' },
+ ],
+ depthStencilAttachment: {
+ view: dsTextureView,
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilClearValue: info.stencil ? 0 : undefined,
+ stencilLoadOp: info.stencil ? 'clear' : undefined,
+ stencilStoreOp: info.stencil ? 'discard' : undefined,
+ },
+ });
+ pass.setPipeline(checkPipeline);
+ pass.setViewport(0, 0, kNumTestPoints, 1, 0.0, 1.0);
+ pass.draw(kNumTestPoints);
+ pass.end();
+ }
+ enc.copyTextureToBuffer({ texture: checkTexture }, { buffer: checkBuffer }, [kNumTestPoints]);
+ if (dsExpected) {
+ enc.copyTextureToBuffer({ texture: dsTexture }, { buffer: dsExpected }, [kNumTestPoints]);
+ }
+ t.device.queue.submit([enc.finish()]);
+
+ t.expectGPUBufferValuesPassCheck(
+ fragInputZFailedBuffer,
+ a => checkElementsBetween(a, [() => -1e-5, () => 1e-5]),
+ { type: Float32Array, typedLength: kNumTestPoints }
+ );
+
+ const kCheckPassedValue = 0;
+ const predicatePrinter: CheckElementsSupplementalTableRows = [
+ { leftHeader: 'expected ==', getValueForCell: index => kCheckPassedValue },
+ ];
+ if (dsActual && dsExpected && format === 'depth32float') {
+ await Promise.all([dsActual.mapAsync(GPUMapMode.READ), dsExpected.mapAsync(GPUMapMode.READ)]);
+ const act = new Float32Array(dsActual.getMappedRange());
+ const exp = new Float32Array(dsExpected.getMappedRange());
+ predicatePrinter.push(
+ { leftHeader: 'act ==', getValueForCell: index => act[index].toFixed(2) },
+ { leftHeader: 'exp ==', getValueForCell: index => exp[index].toFixed(2) }
+ );
+ }
+ t.expectGPUBufferValuesPassCheck(
+ checkBuffer,
+ a =>
+ checkElementsPassPredicate(a, (index, value) => value === kCheckPassedValue, {
+ predicatePrinter,
+ }),
+ { type: Uint8Array, typedLength: kNumTestPoints, method: 'map' }
+ );
+ });
+
+g.test('depth_test_input_clamped')
+ .desc(
+ `
+Input to the depth test should always be in the range of viewport depth, even if it was written by
+the fragment shader (using frag_depth).
+
+To test this, first initialize the depth buffer with N expected values (by writing frag_depth, with
+the default viewport). These expected values are clamped by the shader to [0.25, 0.75].
+
+Then, run another pass with the viewport depth set to [0.25,0.75], and output various (unclamped)
+frag_depth values from its fragment shader with depthCompare:'not-equal'. These should get clamped;
+any fragments that have unexpected values then get drawn to the color buffer, which is later checked
+to be empty.`
+ )
+ .params(u =>
+ u //
+ .combine('format', kDepthStencilFormats)
+ .filter(p => kTextureFormatInfo[p.format].depth)
+ .combine('unclippedDepth', [false, true])
+ .combine('multisampled', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+
+ t.selectDeviceOrSkipTestCase([
+ t.params.unclippedDepth ? 'depth-clip-control' : undefined,
+ info.feature,
+ ]);
+ })
+ .fn(async t => {
+ const { format, unclippedDepth, multisampled } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const kNumDepthValues = 8;
+ const kViewportMinDepth = 0.25;
+ const kViewportMaxDepth = 0.75;
+
+ const shaderSource = `
+ // Test depths, with viewport range corresponding to [0,1].
+ var<private> kDepths: array<f32, ${kNumDepthValues}> = array<f32, ${kNumDepthValues}>(
+ -1.0, -0.5, 0.0, 0.25, 0.75, 1.0, 1.5, 2.0);
+
+ const vpMin: f32 = ${kViewportMinDepth};
+ const vpMax: f32 = ${kViewportMaxDepth};
+
+ // Draw the points in a straight horizontal row, one per pixel.
+ fn vertexX(idx: u32) -> f32 {
+ return (f32(idx) + 0.5) * 2.0 / ${kNumDepthValues}.0 - 1.0;
+ }
+
+ struct VF {
+ @builtin(position) pos: vec4<f32>,
+ @location(0) @interpolate(flat) vertexIndex: u32,
+ };
+
+ @vertex
+ fn vmain(@builtin(vertex_index) idx: u32) -> VF {
+ var vf: VF;
+ // Depth=0.5 because we want to render every point, not get clipped.
+ vf.pos = vec4<f32>(vertexX(idx), 0.0, 0.5, 1.0);
+ vf.vertexIndex = idx;
+ return vf;
+ }
+
+ @fragment
+ fn finit(vf: VF) -> @builtin(frag_depth) f32 {
+ // Expected values of the ftest pipeline.
+ return clamp(kDepths[vf.vertexIndex], vpMin, vpMax);
+ }
+
+ struct FTest {
+ @builtin(frag_depth) depth: f32,
+ @location(0) color: f32,
+ };
+
+ @fragment
+ fn ftest(vf: VF) -> FTest {
+ var f: FTest;
+ f.depth = kDepths[vf.vertexIndex]; // Should get clamped to the viewport.
+ f.color = 1.0; // Color written if the resulting depth is unexpected.
+ return f;
+ }
+ `;
+
+ const module = t.device.createShaderModule({ code: shaderSource });
+
+ // Initialize depth attachment with expected values, in [0.25,0.75].
+ const initPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain' },
+ primitive: { topology: 'point-list' },
+ depthStencil: { format, depthWriteEnabled: true },
+ multisample: multisampled ? { count: 4 } : undefined,
+ fragment: { module, entryPoint: 'finit', targets: [] },
+ });
+
+ // With a viewport set to [0.25,0.75], output values in [0.0,1.0] and check they're clamped
+ // before the depth test, regardless of whether unclippedDepth is enabled.
+ const testPipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module, entryPoint: 'vmain' },
+ primitive: {
+ topology: 'point-list',
+ unclippedDepth,
+ },
+ depthStencil: { format, depthCompare: 'not-equal' },
+ multisample: multisampled ? { count: 4 } : undefined,
+ fragment: { module, entryPoint: 'ftest', targets: [{ format: 'r8unorm' }] },
+ });
+
+ const dsTexture = t.device.createTexture({
+ format,
+ size: [kNumDepthValues],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ sampleCount: multisampled ? 4 : 1,
+ });
+ const dsTextureView = dsTexture.createView();
+
+ const testTextureDesc = {
+ format: 'r8unorm' as const,
+ size: [kNumDepthValues],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ };
+ const testTexture = t.device.createTexture(testTextureDesc);
+ const testTextureView = testTexture.createView();
+ const testTextureMSView = multisampled
+ ? t.device.createTexture({ ...testTextureDesc, sampleCount: 4 }).createView()
+ : undefined;
+
+ const resultBuffer = t.device.createBuffer({
+ size: kNumDepthValues,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ });
+
+ const enc = t.device.createCommandEncoder();
+ {
+ const pass = enc.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: dsTextureView,
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: info.stencil ? 0 : undefined,
+ stencilLoadOp: info.stencil ? 'clear' : undefined,
+ stencilStoreOp: info.stencil ? 'discard' : undefined,
+ },
+ });
+ pass.setPipeline(initPipeline);
+ pass.draw(kNumDepthValues);
+ pass.end();
+ }
+ {
+ const clearValue = [0, 0, 0, 0]; // Will see this color if the test passed.
+ const pass = enc.beginRenderPass({
+ colorAttachments: [
+ testTextureMSView
+ ? {
+ view: testTextureMSView,
+ resolveTarget: testTextureView,
+ clearValue,
+ loadOp: 'clear',
+ storeOp: 'discard',
+ }
+ : { view: testTextureView, clearValue, loadOp: 'clear', storeOp: 'store' },
+ ],
+ depthStencilAttachment: {
+ view: dsTextureView,
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilClearValue: info.stencil ? 0 : undefined,
+ stencilLoadOp: info.stencil ? 'clear' : undefined,
+ stencilStoreOp: info.stencil ? 'discard' : undefined,
+ },
+ });
+ pass.setPipeline(testPipeline);
+ pass.setViewport(0, 0, kNumDepthValues, 1, kViewportMinDepth, kViewportMaxDepth);
+ pass.draw(kNumDepthValues);
+ pass.end();
+ }
+ enc.copyTextureToBuffer({ texture: testTexture }, { buffer: resultBuffer }, [kNumDepthValues]);
+ t.device.queue.submit([enc.finish()]);
+
+ t.expectGPUBufferValuesEqual(resultBuffer, new Uint8Array(kNumDepthValues), 0, {
+ method: 'map',
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/draw.spec.ts
new file mode 100644
index 0000000000..5dd500c054
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/draw.spec.ts
@@ -0,0 +1,750 @@
+export const description = `
+Tests for the general aspects of draw/drawIndexed/drawIndirect/drawIndexedIndirect.
+
+Primitive topology tested in api/operation/render_pipeline/primitive_topology.spec.ts.
+Index format tested in api/operation/command_buffer/render/state_tracking.spec.ts.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ assert,
+ TypedArrayBufferView,
+ TypedArrayBufferViewConstructor,
+} from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+class DrawTest extends GPUTest {
+ checkTriangleDraw(opts: {
+ firstIndex: number | undefined;
+ count: number;
+ firstInstance: number | undefined;
+ instanceCount: number | undefined;
+ indexed: boolean;
+ indirect: boolean;
+ vertexBufferOffset: number;
+ indexBufferOffset: number | undefined;
+ baseVertex: number | undefined;
+ }): void {
+ // Set fallbacks when parameters are undefined in order to calculate the expected values.
+ const defaulted = {
+ firstIndex: opts.firstIndex ?? 0,
+ count: opts.count,
+ firstInstance: opts.firstInstance ?? 0,
+ instanceCount: opts.instanceCount ?? 1,
+ indexed: opts.indexed,
+ indirect: opts.indirect,
+ vertexBufferOffset: opts.vertexBufferOffset,
+ indexBufferOffset: opts.indexBufferOffset ?? 0,
+ baseVertex: opts.baseVertex ?? 0,
+ };
+
+ const renderTargetSize = [72, 36];
+
+ // The test will split up the render target into a grid where triangles of
+ // increasing primitive id will be placed along the X axis, and triangles
+ // of increasing instance id will be placed along the Y axis. The size of the
+ // grid is based on the max primitive id and instance id used.
+ const numX = 6;
+ const numY = 6;
+ const tileSizeX = renderTargetSize[0] / numX;
+ const tileSizeY = renderTargetSize[1] / numY;
+
+ // |\
+ // | \
+ // |______\
+ // Unit triangle shaped like this. 0-1 Y-down.
+ const triangleVertices = /* prettier-ignore */ [
+ 0.0, 0.0,
+ 0.0, 1.0,
+ 1.0, 1.0,
+ ];
+
+ const renderTarget = this.device.createTexture({
+ size: renderTargetSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+
+ const vertexModule = this.device.createShaderModule({
+ code: `
+struct Inputs {
+ @builtin(vertex_index) vertex_index : u32,
+ @builtin(instance_index) instance_id : u32,
+ @location(0) vertexPosition : vec2<f32>,
+};
+
+@vertex fn vert_main(input : Inputs
+ ) -> @builtin(position) vec4<f32> {
+ // 3u is the number of points in a triangle to convert from index
+ // to id.
+ var vertex_id : u32 = input.vertex_index / 3u;
+
+ var x : f32 = (input.vertexPosition.x + f32(vertex_id)) / ${numX}.0;
+ var y : f32 = (input.vertexPosition.y + f32(input.instance_id)) / ${numY}.0;
+
+ // (0,1) y-down space to (-1,1) y-up NDC
+ x = 2.0 * x - 1.0;
+ y = -2.0 * y + 1.0;
+ return vec4<f32>(x, y, 0.0, 1.0);
+}
+`,
+ });
+
+ const fragmentModule = this.device.createShaderModule({
+ code: `
+struct Output {
+ value : u32
+};
+
+@group(0) @binding(0) var<storage, read_write> output : Output;
+
+@fragment fn frag_main() -> @location(0) vec4<f32> {
+ output.value = 1u;
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+}
+`,
+ });
+
+ const pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: vertexModule,
+ entryPoint: 'vert_main',
+ buffers: [
+ {
+ attributes: [
+ {
+ shaderLocation: 0,
+ format: 'float32x2',
+ offset: 0,
+ },
+ ],
+ arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT,
+ },
+ ],
+ },
+ fragment: {
+ module: fragmentModule,
+ entryPoint: 'frag_main',
+ targets: [
+ {
+ format: 'rgba8unorm',
+ },
+ ],
+ },
+ });
+
+ const resultBuffer = this.device.createBuffer({
+ size: Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const resultBindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: resultBuffer,
+ },
+ },
+ ],
+ });
+
+ const commandEncoder = this.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ renderPass.setPipeline(pipeline);
+ renderPass.setBindGroup(0, resultBindGroup);
+
+ if (defaulted.indexed) {
+ // INDEXED DRAW
+ assert(defaulted.baseVertex !== undefined);
+ assert(defaulted.indexBufferOffset !== undefined);
+
+ renderPass.setIndexBuffer(
+ this.makeBufferWithContents(
+ /* prettier-ignore */ new Uint32Array([
+ // Offset the index buffer contents by empty data.
+ ...new Array(defaulted.indexBufferOffset / Uint32Array.BYTES_PER_ELEMENT),
+
+ 0, 1, 2, //
+ 3, 4, 5, //
+ 6, 7, 8, //
+ ]),
+ GPUBufferUsage.INDEX
+ ),
+ 'uint32',
+ defaulted.indexBufferOffset
+ );
+
+ renderPass.setVertexBuffer(
+ 0,
+ this.makeBufferWithContents(
+ /* prettier-ignore */ new Float32Array([
+ // Offset the vertex buffer contents by empty data.
+ ...new Array(defaulted.vertexBufferOffset / Float32Array.BYTES_PER_ELEMENT),
+
+ // selected with base_vertex=0
+ // count=6
+ ...triangleVertices, // | count=6;first=3
+ ...triangleVertices, // | |
+ ...triangleVertices, // |
+
+ // selected with base_vertex=9
+ // count=6
+ ...triangleVertices, // | count=6;first=3
+ ...triangleVertices, // | |
+ ...triangleVertices, // |
+ ]),
+ GPUBufferUsage.VERTEX
+ ),
+ defaulted.vertexBufferOffset
+ );
+
+ if (defaulted.indirect) {
+ const args = [
+ defaulted.count,
+ defaulted.instanceCount,
+ defaulted.firstIndex,
+ defaulted.baseVertex,
+ defaulted.firstInstance,
+ ] as const;
+ renderPass.drawIndexedIndirect(
+ this.makeBufferWithContents(new Uint32Array(args), GPUBufferUsage.INDIRECT),
+ 0
+ );
+ } else {
+ const args = [
+ opts.count,
+ opts.instanceCount,
+ opts.firstIndex,
+ opts.baseVertex,
+ opts.firstInstance,
+ ] as const;
+ renderPass.drawIndexed.apply(renderPass, [...args]);
+ }
+ } else {
+ // NON-INDEXED DRAW
+ renderPass.setVertexBuffer(
+ 0,
+ this.makeBufferWithContents(
+ /* prettier-ignore */ new Float32Array([
+ // Offset the vertex buffer contents by empty data.
+ ...new Array(defaulted.vertexBufferOffset / Float32Array.BYTES_PER_ELEMENT),
+
+ // count=6
+ ...triangleVertices, // | count=6;first=3
+ ...triangleVertices, // | |
+ ...triangleVertices, // |
+ ]),
+ GPUBufferUsage.VERTEX
+ ),
+ defaulted.vertexBufferOffset
+ );
+
+ if (defaulted.indirect) {
+ const args = [
+ defaulted.count,
+ defaulted.instanceCount,
+ defaulted.firstIndex,
+ defaulted.firstInstance,
+ ] as const;
+ renderPass.drawIndirect(
+ this.makeBufferWithContents(new Uint32Array(args), GPUBufferUsage.INDIRECT),
+ 0
+ );
+ } else {
+ const args = [opts.count, opts.instanceCount, opts.firstIndex, opts.firstInstance] as const;
+ renderPass.draw.apply(renderPass, [...args]);
+ }
+ }
+
+ renderPass.end();
+ this.queue.submit([commandEncoder.finish()]);
+
+ const green = new Uint8Array([0, 255, 0, 255]);
+ const transparentBlack = new Uint8Array([0, 0, 0, 0]);
+
+ const didDraw = defaulted.count && defaulted.instanceCount;
+
+ this.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array([didDraw ? 1 : 0]));
+
+ const baseVertexCount = defaulted.baseVertex ?? 0;
+ for (let primitiveId = 0; primitiveId < numX; ++primitiveId) {
+ for (let instanceId = 0; instanceId < numY; ++instanceId) {
+ let expectedColor = didDraw ? green : transparentBlack;
+ if (
+ primitiveId * 3 < defaulted.firstIndex + baseVertexCount ||
+ primitiveId * 3 >= defaulted.firstIndex + baseVertexCount + defaulted.count
+ ) {
+ expectedColor = transparentBlack;
+ }
+
+ if (
+ instanceId < defaulted.firstInstance ||
+ instanceId >= defaulted.firstInstance + defaulted.instanceCount
+ ) {
+ expectedColor = transparentBlack;
+ }
+
+ this.expectSinglePixelIn2DTexture(
+ renderTarget,
+ 'rgba8unorm',
+ {
+ x: (1 / 3 + primitiveId) * tileSizeX,
+ y: (2 / 3 + instanceId) * tileSizeY,
+ },
+ {
+ exp: expectedColor,
+ }
+ );
+ }
+ }
+ }
+}
+
+export const g = makeTestGroup(DrawTest);
+
+g.test('arguments')
+ .desc(
+ `Test that draw arguments are passed correctly by drawing triangles in a grid.
+Horizontally across the texture are triangles with increasing "primitive id".
+Vertically down the screen are triangles with increasing instance id.
+Increasing the |first| param should skip some of the beginning triangles on the horizontal axis.
+Increasing the |first_instance| param should skip of the beginning triangles on the vertical axis.
+The vertex buffer contains two sets of disjoint triangles, and base_vertex is used to select the second set.
+The test checks that the center of all of the expected triangles is drawn, and the others are empty.
+The fragment shader also writes out to a storage buffer. If the draw is zero-sized, check that no value is written.
+
+Params:
+ - first= {0, 3} - either the firstVertex or firstIndex
+ - count= {0, 3, 6} - either the vertexCount or indexCount
+ - first_instance= {0, 2}
+ - instance_count= {0, 1, 4}
+ - indexed= {true, false}
+ - indirect= {true, false}
+ - vertex_buffer_offset= {0, 32}
+ - index_buffer_offset= {0, 16} - only for indexed draws
+ - base_vertex= {0, 9} - only for indexed draws
+ `
+ )
+ .params(u =>
+ u
+ .combine('first', [0, 3] as const)
+ .combine('count', [0, 3, 6] as const)
+ .combine('first_instance', [0, 2] as const)
+ .combine('instance_count', [0, 1, 4] as const)
+ .combine('indexed', [false, true])
+ .combine('indirect', [false, true])
+ .combine('vertex_buffer_offset', [0, 32] as const)
+ .expand('index_buffer_offset', p => (p.indexed ? ([0, 16] as const) : [undefined]))
+ .expand('base_vertex', p => (p.indexed ? ([0, 9] as const) : [undefined]))
+ )
+ .beforeAllSubcases(t => {
+ if (t.params.first_instance > 0 && t.params.indirect) {
+ t.selectDeviceOrSkipTestCase('indirect-first-instance');
+ }
+ })
+ .fn(async t => {
+ t.checkTriangleDraw({
+ firstIndex: t.params.first,
+ count: t.params.count,
+ firstInstance: t.params.first_instance,
+ instanceCount: t.params.instance_count,
+ indexed: t.params.indexed,
+ indirect: t.params.indirect,
+ vertexBufferOffset: t.params.vertex_buffer_offset,
+ indexBufferOffset: t.params.index_buffer_offset,
+ baseVertex: t.params.base_vertex,
+ });
+ });
+
+g.test('default_arguments')
+ .desc(
+ `
+ Test that defaults arguments are passed correctly by drawing triangles in a grid when they are not
+ defined. This test is written based on the 'arguments' with 'undefined' value in the parameters.
+ - mode= {draw, drawIndexed}
+ - arg= {instance_count, first_index, first_instance, base_vertex}
+ `
+ )
+ .params(u =>
+ u
+ .combine('mode', ['draw', 'drawIndexed'])
+ .beginSubcases()
+ .combine('instance_count', [undefined, 4] as const)
+ .combine('first_index', [undefined, 3] as const)
+ .combine('first_instance', [undefined, 2] as const)
+ .expand('base_vertex', p =>
+ p.mode === 'drawIndexed' ? ([undefined, 9] as const) : [undefined]
+ )
+ )
+ .fn(async t => {
+ const kVertexCount = 3;
+ const kVertexBufferOffset = 32;
+ const kIndexBufferOffset = 16;
+
+ t.checkTriangleDraw({
+ firstIndex: t.params.first_index,
+ count: kVertexCount,
+ firstInstance: t.params.first_instance,
+ instanceCount: t.params.instance_count,
+ indexed: t.params.mode === 'drawIndexed',
+ indirect: false, // indirect
+ vertexBufferOffset: kVertexBufferOffset,
+ indexBufferOffset: kIndexBufferOffset,
+ baseVertex: t.params.base_vertex,
+ });
+ });
+
+g.test('vertex_attributes,basic')
+ .desc(
+ `Test basic fetching of vertex attributes.
+ Each vertex attribute is a single value and written out into a storage buffer.
+ Tests that vertices with offsets/strides for instanced/non-instanced attributes are
+ fetched correctly. Not all vertex formats are tested.
+
+ Params:
+ - vertex_attribute_count= {1, 4, 8, 16}
+ - vertex_buffer_count={1, 4, 8} - where # attributes is > 0
+ - vertex_format={uint32, float32}
+ - step_mode= {undefined, vertex, instance, mixed} - where mixed only applies for vertex_buffer_count > 1
+ `
+ )
+ .params(u =>
+ u
+ .combine('vertex_attribute_count', [1, 4, 8, 16])
+ .combine('vertex_buffer_count', [1, 4, 8])
+ .combine('vertex_format', ['uint32', 'float32'] as const)
+ .combine('step_mode', [undefined, 'vertex', 'instance', 'mixed'] as const)
+ .unless(p => p.vertex_attribute_count < p.vertex_buffer_count)
+ .unless(p => p.step_mode === 'mixed' && p.vertex_buffer_count <= 1)
+ )
+ .fn(t => {
+ const vertexCount = 4;
+ const instanceCount = 4;
+
+ const attributesPerVertexBuffer =
+ t.params.vertex_attribute_count / t.params.vertex_buffer_count;
+ assert(Math.round(attributesPerVertexBuffer) === attributesPerVertexBuffer);
+
+ let shaderLocation = 0;
+ let attributeValue = 0;
+ const bufferLayouts: GPUVertexBufferLayout[] = [];
+
+ let ExpectedDataConstructor: TypedArrayBufferViewConstructor;
+ switch (t.params.vertex_format) {
+ case 'uint32':
+ ExpectedDataConstructor = Uint32Array;
+ break;
+ case 'float32':
+ ExpectedDataConstructor = Float32Array;
+ break;
+ }
+
+ // Populate |bufferLayouts|, |vertexBufferData|, and |vertexBuffers|.
+ // We will use this to both create the render pipeline, and produce the
+ // expected data on the CPU.
+ // Attributes in each buffer will be interleaved.
+ const vertexBuffers: GPUBuffer[] = [];
+ const vertexBufferData: TypedArrayBufferView[] = [];
+ for (let b = 0; b < t.params.vertex_buffer_count; ++b) {
+ const vertexBufferValues: number[] = [];
+
+ let offset = 0;
+ let stepMode = t.params.step_mode;
+
+ // If stepMode is mixed, alternate between vertex and instance.
+ if (stepMode === 'mixed') {
+ stepMode = (['vertex', 'instance'] as const)[b % 2];
+ }
+
+ let vertexOrInstanceCount: number;
+ switch (stepMode) {
+ case undefined:
+ case 'vertex':
+ vertexOrInstanceCount = vertexCount;
+ break;
+ case 'instance':
+ vertexOrInstanceCount = instanceCount;
+ break;
+ }
+
+ const attributes: GPUVertexAttribute[] = [];
+ for (let a = 0; a < attributesPerVertexBuffer; ++a) {
+ const attribute: GPUVertexAttribute = {
+ format: t.params.vertex_format,
+ shaderLocation,
+ offset,
+ };
+ attributes.push(attribute);
+
+ offset += ExpectedDataConstructor.BYTES_PER_ELEMENT;
+ shaderLocation += 1;
+ }
+
+ for (let v = 0; v < vertexOrInstanceCount; ++v) {
+ for (let a = 0; a < attributesPerVertexBuffer; ++a) {
+ vertexBufferValues.push(attributeValue);
+ attributeValue += 1.234; // Values will get rounded later if we make a Uint32Array.
+ }
+ }
+
+ bufferLayouts.push({
+ attributes,
+ arrayStride: offset,
+ stepMode,
+ });
+
+ const data = new ExpectedDataConstructor(vertexBufferValues);
+ vertexBufferData.push(data);
+ vertexBuffers.push(t.makeBufferWithContents(data, GPUBufferUsage.VERTEX));
+ }
+
+ // Create an array of shader locations [0, 1, 2, 3, ...] for easy iteration.
+ const vertexInputShaderLocations = new Array(shaderLocation).fill(0).map((_, i) => i);
+
+ // Create the expected data buffer.
+ const expectedData = new ExpectedDataConstructor(
+ vertexCount * instanceCount * vertexInputShaderLocations.length
+ );
+
+ // Populate the expected data. This is a CPU-side version of what we expect the shader
+ // to do.
+ for (let vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) {
+ for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+ bufferLayouts.forEach((bufferLayout, b) => {
+ for (const attribute of bufferLayout.attributes) {
+ const primitiveId = vertexCount * instanceIndex + vertexIndex;
+ const outputIndex =
+ primitiveId * vertexInputShaderLocations.length + attribute.shaderLocation;
+
+ let vertexOrInstanceIndex: number;
+ switch (bufferLayout.stepMode) {
+ case undefined:
+ case 'vertex':
+ vertexOrInstanceIndex = vertexIndex;
+ break;
+ case 'instance':
+ vertexOrInstanceIndex = instanceIndex;
+ break;
+ }
+
+ const view = new ExpectedDataConstructor(
+ vertexBufferData[b].buffer,
+ bufferLayout.arrayStride * vertexOrInstanceIndex + attribute.offset,
+ 1
+ );
+ expectedData[outputIndex] = view[0];
+ }
+ });
+ }
+ }
+
+ let wgslFormat: string;
+ switch (t.params.vertex_format) {
+ case 'uint32':
+ wgslFormat = 'u32';
+ break;
+ case 'float32':
+ wgslFormat = 'f32';
+ break;
+ }
+
+ // Maximum inter-stage shader location is 14, and we need to consume one for primitiveId, 12 for
+ // location 0 to 11, and combine the remaining vertex inputs into one location (one
+ // vec4<wgslFormat> when vertex_attribute_count === 16).
+ const interStageScalarShaderLocation = Math.min(shaderLocation, 12);
+ const interStageScalarShaderLocations = new Array(interStageScalarShaderLocation)
+ .fill(0)
+ .map((_, i) => i);
+
+ let accumulateVariableDeclarationsInVertexShader = '';
+ let accumulateVariableAssignmentsInVertexShader = '';
+ let accumulateVariableDeclarationsInFragmentShader = '';
+ let accumulateVariableAssignmentsInFragmentShader = '';
+ // The remaining 3 vertex attributes
+ if (t.params.vertex_attribute_count === 16) {
+ accumulateVariableDeclarationsInVertexShader = `
+ @location(13) @interpolate(flat) outAttrib13 : vec4<${wgslFormat}>,
+ `;
+ accumulateVariableAssignmentsInVertexShader = `
+ output.outAttrib13 =
+ vec4<${wgslFormat}>(input.attrib12, input.attrib13, input.attrib14, input.attrib15);
+ `;
+ accumulateVariableDeclarationsInFragmentShader = `
+ @location(13) @interpolate(flat) attrib13 : vec4<${wgslFormat}>,
+ `;
+ accumulateVariableAssignmentsInFragmentShader = `
+ outBuffer.primitives[input.primitiveId].attrib12 = input.attrib13.x;
+ outBuffer.primitives[input.primitiveId].attrib13 = input.attrib13.y;
+ outBuffer.primitives[input.primitiveId].attrib14 = input.attrib13.z;
+ outBuffer.primitives[input.primitiveId].attrib15 = input.attrib13.w;
+ `;
+ }
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+struct Inputs {
+ @builtin(vertex_index) vertexIndex : u32,
+ @builtin(instance_index) instanceIndex : u32,
+${vertexInputShaderLocations.map(i => ` @location(${i}) attrib${i} : ${wgslFormat},`).join('\n')}
+};
+
+struct Outputs {
+ @builtin(position) Position : vec4<f32>,
+${interStageScalarShaderLocations
+ .map(i => ` @location(${i}) @interpolate(flat) outAttrib${i} : ${wgslFormat},`)
+ .join('\n')}
+ @location(${interStageScalarShaderLocations.length}) @interpolate(flat) primitiveId : u32,
+${accumulateVariableDeclarationsInVertexShader}
+};
+
+@vertex fn main(input : Inputs) -> Outputs {
+ var output : Outputs;
+${interStageScalarShaderLocations.map(i => ` output.outAttrib${i} = input.attrib${i};`).join('\n')}
+${accumulateVariableAssignmentsInVertexShader}
+
+ output.primitiveId = input.instanceIndex * ${instanceCount}u + input.vertexIndex;
+ output.Position = vec4<f32>(0.0, 0.0, 0.5, 1.0);
+ return output;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ buffers: bufferLayouts,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+struct Inputs {
+${interStageScalarShaderLocations
+ .map(i => ` @location(${i}) @interpolate(flat) attrib${i} : ${wgslFormat},`)
+ .join('\n')}
+ @location(${interStageScalarShaderLocations.length}) @interpolate(flat) primitiveId : u32,
+${accumulateVariableDeclarationsInFragmentShader}
+};
+
+struct OutPrimitive {
+${vertexInputShaderLocations.map(i => ` attrib${i} : ${wgslFormat},`).join('\n')}
+};
+struct OutBuffer {
+ primitives : array<OutPrimitive>
+};
+@group(0) @binding(0) var<storage, read_write> outBuffer : OutBuffer;
+
+@fragment fn main(input : Inputs) {
+${interStageScalarShaderLocations
+ .map(i => ` outBuffer.primitives[input.primitiveId].attrib${i} = input.attrib${i};`)
+ .join('\n')}
+${accumulateVariableAssignmentsInFragmentShader}
+}
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ format: 'rgba8unorm',
+ writeMask: 0,
+ },
+ ],
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ });
+
+ const resultBuffer = t.device.createBuffer({
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ size: vertexCount * instanceCount * vertexInputShaderLocations.length * 4,
+ });
+
+ const resultBindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: resultBuffer,
+ },
+ },
+ ],
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ // Dummy render attachment - not used (WebGPU doesn't allow using a render pass with no
+ // attachments)
+ view: t.device
+ .createTexture({
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [1],
+ format: 'rgba8unorm',
+ })
+ .createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ renderPass.setPipeline(pipeline);
+ renderPass.setBindGroup(0, resultBindGroup);
+ for (let i = 0; i < t.params.vertex_buffer_count; ++i) {
+ renderPass.setVertexBuffer(i, vertexBuffers[i]);
+ }
+ renderPass.draw(vertexCount, instanceCount);
+ renderPass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(resultBuffer, expectedData);
+ });
+
+g.test('vertex_attributes,formats')
+ .desc(
+ `Test all vertex formats are fetched correctly.
+
+ Runs a basic vertex shader which loads vertex data from two attributes which
+ may have different formats. Write data out to a storage buffer and check that
+ it was loaded correctly.
+
+ Params:
+ - vertex_format_1={...all_vertex_formats}
+ - vertex_format_2={...all_vertex_formats}
+ `
+ )
+ .unimplemented();
+
+g.test(`largeish_buffer`)
+ .desc(
+ `
+ Test a very large range of buffer is bound.
+ For a render pipeline that use a vertex step mode and a instance step mode vertex buffer, test
+ that :
+ - For draw, drawIndirect, drawIndexed and drawIndexedIndirect:
+ - The bound range of vertex step mode vertex buffer is significantly larger than necessary
+ - The bound range of instance step mode vertex buffer is significantly larger than necessary
+ - A large buffer is bound to an unused slot
+ - For drawIndexed and drawIndexedIndirect:
+ - The bound range of index buffer is significantly larger than necessary
+ - For drawIndirect and drawIndexedIndirect:
+ - The indirect buffer is significantly larger than necessary
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/indirect_draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/indirect_draw.spec.ts
new file mode 100644
index 0000000000..150891e7ef
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/indirect_draw.spec.ts
@@ -0,0 +1,251 @@
+export const description = `
+Tests for the indirect-specific aspects of drawIndirect/drawIndexedIndirect.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ kDrawIndirectParametersSize,
+ kDrawIndexedIndirectParametersSize,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+const filled = new Uint8Array([0, 255, 0, 255]);
+const notFilled = new Uint8Array([0, 0, 0, 0]);
+
+const kRenderTargetFormat = 'rgba8unorm';
+
+class F extends GPUTest {
+ MakeIndexBuffer(): GPUBuffer {
+ return this.makeBufferWithContents(
+ /* prettier-ignore */
+ new Uint32Array([
+ 0, 1, 2, // The bottom left triangle
+ 1, 2, 3, // The top right triangle
+ ]),
+ GPUBufferUsage.INDEX
+ );
+ }
+
+ MakeVertexBuffer(isIndexed: boolean): GPUBuffer {
+ /* prettier-ignore */
+ const vertices = isIndexed
+ ? [
+ -1.0, -1.0,
+ -1.0, 1.0,
+ 1.0, -1.0,
+ 1.0, 1.0,
+ ]
+ : [
+ // The bottom left triangle
+ -1.0, 1.0,
+ 1.0, -1.0,
+ -1.0, -1.0,
+
+ // The top right triangle
+ -1.0, 1.0,
+ 1.0, -1.0,
+ 1.0, 1.0,
+ ];
+ return this.makeBufferWithContents(new Float32Array(vertices), GPUBufferUsage.VERTEX);
+ }
+
+ MakeIndirectBuffer(isIndexed: boolean, indirectOffset: number): GPUBuffer {
+ const o = indirectOffset / Uint32Array.BYTES_PER_ELEMENT;
+
+ const parametersSize = isIndexed
+ ? kDrawIndexedIndirectParametersSize
+ : kDrawIndirectParametersSize;
+ const arraySize = o + parametersSize * 2;
+
+ const indirectBuffer = [...Array(arraySize)].map(() => Math.floor(Math.random() * 100));
+
+ if (isIndexed) {
+ // draw args that will draw the left bottom triangle (expected call)
+ indirectBuffer[o] = 3; // indexCount
+ indirectBuffer[o + 1] = 1; // instanceCount
+ indirectBuffer[o + 2] = 0; // firstIndex
+ indirectBuffer[o + 3] = 0; // baseVertex
+ indirectBuffer[o + 4] = 0; // firstInstance
+
+ // draw args that will draw both triangles
+ indirectBuffer[o + 5] = 6; // indexCount
+ indirectBuffer[o + 6] = 1; // instanceCount
+ indirectBuffer[o + 7] = 0; // firstIndex
+ indirectBuffer[o + 8] = 0; // baseVertex
+ indirectBuffer[o + 9] = 0; // firstInstance
+
+ if (o >= parametersSize) {
+ // draw args that will draw the right top triangle
+ indirectBuffer[o - 5] = 3; // indexCount
+ indirectBuffer[o - 4] = 1; // instanceCount
+ indirectBuffer[o - 3] = 3; // firstIndex
+ indirectBuffer[o - 2] = 0; // baseVertex
+ indirectBuffer[o - 1] = 0; // firstInstance
+ }
+
+ if (o >= parametersSize * 2) {
+ // draw args that will draw nothing
+ indirectBuffer[0] = 0; // indexCount
+ indirectBuffer[1] = 0; // instanceCount
+ indirectBuffer[2] = 0; // firstIndex
+ indirectBuffer[3] = 0; // baseVertex
+ indirectBuffer[4] = 0; // firstInstance
+ }
+ } else {
+ // draw args that will draw the left bottom triangle (expected call)
+ indirectBuffer[o] = 3; // vertexCount
+ indirectBuffer[o + 1] = 1; // instanceCount
+ indirectBuffer[o + 2] = 0; // firstVertex
+ indirectBuffer[o + 3] = 0; // firstInstance
+
+ // draw args that will draw both triangles
+ indirectBuffer[o + 4] = 6; // vertexCount
+ indirectBuffer[o + 5] = 1; // instanceCount
+ indirectBuffer[o + 6] = 0; // firstVertex
+ indirectBuffer[o + 7] = 0; // firstInstance
+
+ if (o >= parametersSize) {
+ // draw args that will draw the right top triangle
+ indirectBuffer[o - 4] = 3; // vertexCount
+ indirectBuffer[o - 3] = 1; // instanceCount
+ indirectBuffer[o - 2] = 3; // firstVertex
+ indirectBuffer[o - 1] = 0; // firstInstance
+ }
+
+ if (o >= parametersSize * 2) {
+ // draw args that will draw nothing
+ indirectBuffer[0] = 0; // vertexCount
+ indirectBuffer[1] = 0; // instanceCount
+ indirectBuffer[2] = 0; // firstVertex
+ indirectBuffer[3] = 0; // firstInstance
+ }
+ }
+
+ return this.makeBufferWithContents(new Uint32Array(indirectBuffer), GPUBufferUsage.INDIRECT);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('basics')
+ .desc(
+ `Test that the indirect draw parameters are tightly packed for drawIndirect and drawIndexedIndirect.
+An indirectBuffer is created based on indirectOffset. The actual draw args being used indicated by the
+indirectOffset is going to draw a left bottom triangle.
+While the remaining indirectBuffer is populated with random numbers or draw args
+that draw right top triangle, both, or nothing which will fail the color check.
+The test will check render target to see if only the left bottom area is filled,
+meaning the expected draw args is uploaded correctly by the indirectBuffer and indirectOffset.
+
+Params:
+ - draw{Indirect, IndexedIndirect}
+ - indirectOffset= {0, 4, k * sizeof(args struct), k * sizeof(args struct) + 4}
+ `
+ )
+ .params(u =>
+ u
+ .combine('isIndexed', [true, false])
+ .beginSubcases()
+ .expand('indirectOffset', p => {
+ const indirectDrawParametersSize = p.isIndexed
+ ? kDrawIndexedIndirectParametersSize * Uint32Array.BYTES_PER_ELEMENT
+ : kDrawIndirectParametersSize * Uint32Array.BYTES_PER_ELEMENT;
+ return [
+ 0,
+ Uint32Array.BYTES_PER_ELEMENT,
+ 1 * indirectDrawParametersSize,
+ 1 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
+ 3 * indirectDrawParametersSize,
+ 3 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
+ 99 * indirectDrawParametersSize,
+ 99 * indirectDrawParametersSize + Uint32Array.BYTES_PER_ELEMENT,
+ ] as const;
+ })
+ )
+ .fn(t => {
+ const { isIndexed, indirectOffset } = t.params;
+
+ const vertexBuffer = t.MakeVertexBuffer(isIndexed);
+ const indirectBuffer = t.MakeIndirectBuffer(isIndexed, indirectOffset);
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `@vertex fn main(@location(0) pos : vec2<f32>) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(pos, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ attributes: [
+ {
+ shaderLocation: 0,
+ format: 'float32x2',
+ offset: 0,
+ },
+ ],
+ arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT,
+ },
+ ],
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ format: kRenderTargetFormat,
+ },
+ ],
+ },
+ });
+
+ const renderTarget = t.device.createTexture({
+ size: [4, 4],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: kRenderTargetFormat,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(pipeline);
+ renderPass.setVertexBuffer(0, vertexBuffer, 0);
+
+ if (isIndexed) {
+ renderPass.setIndexBuffer(t.MakeIndexBuffer(), 'uint32', 0);
+ renderPass.drawIndexedIndirect(indirectBuffer, indirectOffset);
+ } else {
+ renderPass.drawIndirect(indirectBuffer, indirectOffset);
+ }
+ renderPass.end();
+ t.queue.submit([commandEncoder.finish()]);
+
+ // The bottom left area is filled
+ t.expectSinglePixelIn2DTexture(
+ renderTarget,
+ kRenderTargetFormat,
+ { x: 0, y: 1 },
+ { exp: filled }
+ );
+ // The top right area is not filled
+ t.expectSinglePixelIn2DTexture(
+ renderTarget,
+ kRenderTargetFormat,
+ { x: 1, y: 0 },
+ { exp: notFilled }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/robust_access_index.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/robust_access_index.spec.ts
new file mode 100644
index 0000000000..68d7bc795d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/robust_access_index.spec.ts
@@ -0,0 +1,8 @@
+export const description = `
+TODO: Test that drawIndexedIndirect accesses the index buffer robustly.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts
new file mode 100644
index 0000000000..5985616a54
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/rendering/stencil.spec.ts
@@ -0,0 +1,583 @@
+export const description = `
+Test related to stencil states, stencil op, compare func, etc.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { TypedArrayBufferView } from '../../../../common/util/util.js';
+import {
+ DepthStencilFormat,
+ kDepthStencilFormats,
+ kTextureFormatInfo,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+const kStencilFormats = kDepthStencilFormats.filter(format => kTextureFormatInfo[format].stencil);
+
+const kBaseColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
+const kRedStencilColor = new Float32Array([1.0, 0.0, 0.0, 1.0]);
+const kGreenStencilColor = new Float32Array([0.0, 1.0, 0.0, 1.0]);
+
+type TestStates = {
+ state: GPUDepthStencilState;
+ color: Float32Array;
+ stencil: number | undefined;
+};
+
+class StencilTest extends GPUTest {
+ checkStencilOperation(
+ depthStencilFormat: DepthStencilFormat,
+ testStencilState: GPUStencilFaceState,
+ initialStencil: number,
+ _expectedStencil: number,
+ depthCompare: GPUCompareFunction = 'always'
+ ) {
+ const kReferenceStencil = 3;
+
+ const baseStencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ passOp: 'replace',
+ } as const;
+
+ const stencilState = {
+ compare: 'equal',
+ failOp: 'keep',
+ passOp: 'keep',
+ } as const;
+
+ const baseState = {
+ format: depthStencilFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: baseStencilState,
+ stencilBack: baseStencilState,
+ } as const;
+
+ const testState = {
+ format: depthStencilFormat,
+ depthWriteEnabled: false,
+ depthCompare,
+ stencilFront: testStencilState,
+ stencilBack: testStencilState,
+ } as const;
+
+ const testState2 = {
+ format: depthStencilFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ } as const;
+
+ const testStates = [
+ // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1.
+ { state: baseState, color: kBaseColor, stencil: initialStencil },
+ { state: testState, color: kRedStencilColor, stencil: kReferenceStencil },
+ { state: testState2, color: kGreenStencilColor, stencil: _expectedStencil },
+ ];
+ this.runStencilStateTest(depthStencilFormat, testStates, kGreenStencilColor);
+ }
+
+ checkStencilCompareFunction(
+ depthStencilFormat: DepthStencilFormat,
+ compareFunction: GPUCompareFunction,
+ stencilRefValue: number,
+ expectedColor: Float32Array
+ ) {
+ const baseStencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ passOp: 'replace',
+ } as const;
+
+ const stencilState = {
+ compare: compareFunction,
+ failOp: 'keep',
+ passOp: 'keep',
+ } as const;
+
+ const baseState = {
+ format: depthStencilFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: baseStencilState,
+ stencilBack: baseStencilState,
+ } as const;
+
+ const testState = {
+ format: depthStencilFormat,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ } as const;
+
+ const testStates = [
+ // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1.
+ { state: baseState, color: kBaseColor, stencil: 1 },
+ { state: testState, color: kGreenStencilColor, stencil: stencilRefValue },
+ ];
+ this.runStencilStateTest(depthStencilFormat, testStates, expectedColor);
+ }
+
+ runStencilStateTest(
+ depthStencilFormat: DepthStencilFormat,
+ testStates: TestStates[],
+ expectedColor: Float32Array,
+ isSingleEncoderMultiplePass: boolean = false
+ ) {
+ const renderTargetFormat = 'rgba8unorm';
+ const renderTarget = this.device.createTexture({
+ format: renderTargetFormat,
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const depthTexture = this.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: depthStencilFormat,
+ sampleCount: 1,
+ mipLevelCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
+ });
+
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthTexture.createView(),
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'store',
+ };
+
+ const encoder = this.device.createCommandEncoder();
+ let pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ loadOp: 'load',
+ },
+ ],
+ depthStencilAttachment,
+ });
+
+ if (isSingleEncoderMultiplePass) {
+ pass.end();
+ }
+
+ // Draw a triangle with the given stencil reference and the comparison function.
+ // The color will be kGreenStencilColor if the stencil test passes, and kBaseColor if not.
+ for (const test of testStates) {
+ if (isSingleEncoderMultiplePass) {
+ pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ storeOp: 'store',
+ loadOp: 'load',
+ },
+ ],
+ depthStencilAttachment,
+ });
+ }
+ const testPipeline = this.createRenderPipelineForTest(test.state);
+ pass.setPipeline(testPipeline);
+ if (test.stencil !== undefined) {
+ pass.setStencilReference(test.stencil);
+ }
+ pass.setBindGroup(
+ 0,
+ this.createBindGroupForTest(testPipeline.getBindGroupLayout(0), test.color)
+ );
+ pass.draw(1);
+
+ if (isSingleEncoderMultiplePass) {
+ pass.end();
+ }
+ }
+
+ if (!isSingleEncoderMultiplePass) {
+ pass.end();
+ }
+ this.device.queue.submit([encoder.finish()]);
+
+ const expColor = {
+ R: expectedColor[0],
+ G: expectedColor[1],
+ B: expectedColor[2],
+ A: expectedColor[3],
+ };
+ const expTexelView = TexelView.fromTexelsAsColors(renderTargetFormat, coords => expColor);
+
+ const result = textureContentIsOKByT2B(
+ this,
+ { texture: renderTarget },
+ [1, 1],
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ this.eventualExpectOK(result);
+ this.trackForCleanup(renderTarget);
+ }
+
+ createRenderPipelineForTest(depthStencil: GPUDepthStencilState): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ targets: [{ format: 'rgba8unorm' }],
+ module: this.device.createShaderModule({
+ code: `
+ struct Params {
+ color : vec4<f32>
+ }
+ @group(0) @binding(0) var<uniform> params : Params;
+
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(params.color);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ primitive: { topology: 'point-list' },
+ depthStencil,
+ });
+ }
+
+ createBindGroupForTest(layout: GPUBindGroupLayout, data: TypedArrayBufferView): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: this.makeBufferWithContents(data, GPUBufferUsage.UNIFORM),
+ },
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(StencilTest);
+
+g.test('stencil_compare_func')
+ .desc(
+ `
+ Tests that stencil comparison functions with the stencil reference value works as expected.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kStencilFormats)
+ .combineWithParams([
+ { stencilCompare: 'always', stencilRefValue: 0, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'always', stencilRefValue: 1, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'always', stencilRefValue: 2, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'equal', stencilRefValue: 0, _expectedColor: kBaseColor },
+ { stencilCompare: 'equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'equal', stencilRefValue: 2, _expectedColor: kBaseColor },
+ { stencilCompare: 'greater', stencilRefValue: 0, _expectedColor: kBaseColor },
+ { stencilCompare: 'greater', stencilRefValue: 1, _expectedColor: kBaseColor },
+ { stencilCompare: 'greater', stencilRefValue: 2, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'greater-equal', stencilRefValue: 0, _expectedColor: kBaseColor },
+ { stencilCompare: 'greater-equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'greater-equal', stencilRefValue: 2, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'less', stencilRefValue: 0, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'less', stencilRefValue: 1, _expectedColor: kBaseColor },
+ { stencilCompare: 'less', stencilRefValue: 2, _expectedColor: kBaseColor },
+ { stencilCompare: 'less-equal', stencilRefValue: 0, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'less-equal', stencilRefValue: 1, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'less-equal', stencilRefValue: 2, _expectedColor: kBaseColor },
+ { stencilCompare: 'never', stencilRefValue: 0, _expectedColor: kBaseColor },
+ { stencilCompare: 'never', stencilRefValue: 1, _expectedColor: kBaseColor },
+ { stencilCompare: 'never', stencilRefValue: 2, _expectedColor: kBaseColor },
+ { stencilCompare: 'not-equal', stencilRefValue: 0, _expectedColor: kGreenStencilColor },
+ { stencilCompare: 'not-equal', stencilRefValue: 1, _expectedColor: kBaseColor },
+ { stencilCompare: 'not-equal', stencilRefValue: 2, _expectedColor: kGreenStencilColor },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format, stencilCompare, stencilRefValue, _expectedColor } = t.params;
+
+ t.checkStencilCompareFunction(format, stencilCompare, stencilRefValue, _expectedColor);
+ });
+
+g.test('stencil_passOp_operation')
+ .desc(
+ `
+ Test that the stencil operation is executed on stencil pass. A triangle is drawn with the 'always'
+ comparison function, so it should pass. Then, test that each pass stencil operation works with the
+ given stencil values correctly as expected. For example,
+ - If the pass operation is 'keep', it keeps the initial stencil value.
+ - If the pass operation is 'replace', it replaces the initial stencil value with the reference
+ stencil value.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kStencilFormats)
+ .combineWithParams([
+ { passOp: 'keep', initialStencil: 1, _expectedStencil: 1 },
+ { passOp: 'zero', initialStencil: 1, _expectedStencil: 0 },
+ { passOp: 'replace', initialStencil: 1, _expectedStencil: 3 },
+ { passOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f },
+ { passOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 },
+ { passOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff },
+ { passOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 },
+ { passOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 },
+ { passOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 },
+ { passOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 },
+ { passOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 },
+ { passOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format, passOp, initialStencil, _expectedStencil } = t.params;
+
+ const stencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ passOp,
+ } as const;
+
+ t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil);
+ });
+
+g.test('stencil_failOp_operation')
+ .desc(
+ `
+ Test that the stencil operation is executed on stencil fail. A triangle is drawn with the 'never'
+ comparison function, so it should fail. Then, test that each fail stencil operation works with the
+ given stencil values correctly as expected. For example,
+ - If the fail operation is 'keep', it keeps the initial stencil value.
+ - If the fail operation is 'replace', it replaces the initial stencil value with the reference
+ stencil value.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kStencilFormats)
+ .combineWithParams([
+ { failOp: 'keep', initialStencil: 1, _expectedStencil: 1 },
+ { failOp: 'zero', initialStencil: 1, _expectedStencil: 0 },
+ { failOp: 'replace', initialStencil: 1, _expectedStencil: 3 },
+ { failOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f },
+ { failOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 },
+ { failOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff },
+ { failOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 },
+ { failOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 },
+ { failOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 },
+ { failOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 },
+ { failOp: 'decrement-wrap', initialStencil: 2, _expectedStencil: 1 },
+ { failOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 },
+ { failOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format, failOp, initialStencil, _expectedStencil } = t.params;
+
+ const stencilState = {
+ compare: 'never',
+ failOp,
+ passOp: 'keep',
+ } as const;
+
+ // Draw the base triangle with stencil reference 1. This clears the stencil buffer to 1.
+ // Always fails because the comparison never passes. Therefore red is never drawn, and the
+ // stencil contents may be updated according to `operation`.
+ t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil);
+ });
+
+g.test('stencil_depthFailOp_operation')
+ .desc(
+ `
+ Test that the stencil operation is executed on depthCompare fail. A triangle is drawn with the
+ 'never' depthCompare, so it should fail the depth test. Then, test that each 'depthFailOp' stencil operation
+ works with the given stencil values correctly as expected. For example,
+ - If the depthFailOp operation is 'keep', it keeps the initial stencil value.
+ - If the depthFailOp operation is 'replace', it replaces the initial stencil value with the
+ reference stencil value.
+ `
+ )
+ .params(u =>
+ u //
+ .combine(
+ 'format',
+ kDepthStencilFormats.filter(format => {
+ const info = kTextureFormatInfo[format];
+ return info.depth && info.stencil;
+ })
+ )
+ .combineWithParams([
+ { depthFailOp: 'keep', initialStencil: 1, _expectedStencil: 1 },
+ { depthFailOp: 'zero', initialStencil: 1, _expectedStencil: 0 },
+ { depthFailOp: 'replace', initialStencil: 1, _expectedStencil: 3 },
+ { depthFailOp: 'invert', initialStencil: 0xf0, _expectedStencil: 0x0f },
+ { depthFailOp: 'increment-clamp', initialStencil: 1, _expectedStencil: 2 },
+ { depthFailOp: 'increment-clamp', initialStencil: 0xff, _expectedStencil: 0xff },
+ { depthFailOp: 'increment-wrap', initialStencil: 1, _expectedStencil: 2 },
+ { depthFailOp: 'increment-wrap', initialStencil: 0xff, _expectedStencil: 0 },
+ { depthFailOp: 'decrement-clamp', initialStencil: 1, _expectedStencil: 0 },
+ { depthFailOp: 'decrement-clamp', initialStencil: 0, _expectedStencil: 0 },
+ { depthFailOp: 'decrement-wrap', initialStencil: 2, _expectedStencil: 1 },
+ { depthFailOp: 'decrement-wrap', initialStencil: 1, _expectedStencil: 0 },
+ { depthFailOp: 'decrement-wrap', initialStencil: 0, _expectedStencil: 0xff },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format, depthFailOp, initialStencil, _expectedStencil } = t.params;
+
+ const stencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ passOp: 'keep',
+ depthFailOp,
+ } as const;
+
+ // Call checkStencilOperation function with enabling the depthTest to test that the depthFailOp
+ // stencil operation works as expected.
+ t.checkStencilOperation(format, stencilState, initialStencil, _expectedStencil, 'never');
+ });
+
+g.test('stencil_read_write_mask')
+ .desc(
+ `
+ Tests that setting a stencil read/write masks work. Basically, The base triangle sets 3 to the
+ stencil, and then try to draw a triangle with different stencil values.
+ - In case that 'write' mask is 1,
+ * If the stencil of the triangle is 1, it draws because
+ 'base stencil(3) & write mask(1) == triangle stencil(1)'.
+ * If the stencil of the triangle is 2, it does not draw because
+ 'base stencil(3) & write mask(1) != triangle stencil(2)'.
+
+ - In case that 'read' mask is 2,
+ * If the stencil of the triangle is 1, it does not draw because
+ 'base stencil(3) & read mask(2) != triangle stencil(1)'.
+ * If the stencil of the triangle is 2, it draws because
+ 'base stencil(3) & read mask(2) == triangle stencil(2)'.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kStencilFormats)
+ .combineWithParams([
+ { maskType: 'write', stencilRefValue: 1, _expectedColor: kRedStencilColor },
+ { maskType: 'write', stencilRefValue: 2, _expectedColor: kBaseColor },
+ { maskType: 'read', stencilRefValue: 1, _expectedColor: kBaseColor },
+ { maskType: 'read', stencilRefValue: 2, _expectedColor: kRedStencilColor },
+ ])
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format, maskType, stencilRefValue, _expectedColor } = t.params;
+
+ const baseStencilState = {
+ compare: 'always',
+ failOp: 'keep',
+ passOp: 'replace',
+ } as const;
+
+ const stencilState = {
+ compare: 'equal',
+ failOp: 'keep',
+ passOp: 'keep',
+ } as const;
+
+ const baseState = {
+ format,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: baseStencilState,
+ stencilBack: baseStencilState,
+ stencilReadMask: 0xff,
+ stencilWriteMask: maskType === 'write' ? 0x1 : 0xff,
+ } as const;
+
+ const testState = {
+ format,
+ depthWriteEnabled: false,
+ depthCompare: 'always',
+ stencilFront: stencilState,
+ stencilBack: stencilState,
+ stencilReadMask: maskType === 'read' ? 0x2 : 0xff,
+ stencilWriteMask: 0xff,
+ } as const;
+
+ const testStates = [
+ // Draw the base triangle with stencil reference 3. This clears the stencil buffer to 3.
+ { state: baseState, color: kBaseColor, stencil: 3 },
+ { state: testState, color: kRedStencilColor, stencil: stencilRefValue },
+ ];
+
+ t.runStencilStateTest(format, testStates, _expectedColor);
+ });
+
+g.test('stencil_reference_initialized')
+ .desc('Test that stencil reference is initialized as zero for new render pass.')
+ .params(u => u.combine('format', kStencilFormats))
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format } = t.params;
+
+ const baseStencilState = {
+ compare: 'always',
+ passOp: 'replace',
+ } as const;
+
+ const testStencilState = {
+ compare: 'equal',
+ passOp: 'keep',
+ } as const;
+
+ const baseState = {
+ format,
+ stencilFront: baseStencilState,
+ stencilBack: baseStencilState,
+ } as const;
+
+ const testState = {
+ format,
+ stencilFront: testStencilState,
+ stencilBack: testStencilState,
+ } as const;
+
+ // First pass sets the stencil to 0x1, the second pass sets the stencil to its default
+ // value, and the third pass tests if the stencil is zero.
+ const testStates = [
+ { state: baseState, color: kBaseColor, stencil: 0x1 },
+ { state: baseState, color: kRedStencilColor, stencil: undefined },
+ { state: testState, color: kGreenStencilColor, stencil: 0x0 },
+ ];
+
+ // The third draw should pass the stencil test since the second pass set it to default zero.
+ t.runStencilStateTest(format, testStates, kGreenStencilColor, true);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/buffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/buffer.spec.ts
new file mode 100644
index 0000000000..73c50b8393
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/buffer.spec.ts
@@ -0,0 +1,899 @@
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../common/util/util.js';
+import { GPUConst } from '../../../constants.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { getTextureCopyLayout } from '../../../util/texture/layout.js';
+import { PerTexelComponent } from '../../../util/texture/texel_data.js';
+
+export const description = `
+Test uninitialized buffers are initialized to zero when read
+(or read-written, e.g. with depth write or atomics).
+
+Note that:
+- We don't need 'copy_buffer_to_buffer_copy_destination' here because there has already been an
+ operation test 'command_buffer.copyBufferToBuffer.single' that provides the same functionality.
+`;
+
+const kMapModeOptions = [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE];
+const kBufferUsagesForMappedAtCreationTests = [
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ,
+ GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.MAP_WRITE,
+ GPUConst.BufferUsage.COPY_SRC,
+];
+
+class F extends GPUTest {
+ GetBufferUsageFromMapMode(mapMode: GPUMapModeFlags): number {
+ switch (mapMode) {
+ case GPUMapMode.READ:
+ return GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
+ case GPUMapMode.WRITE:
+ return GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE;
+ default:
+ unreachable();
+ return 0;
+ }
+ }
+
+ async CheckGPUBufferContent(
+ buffer: GPUBuffer,
+ bufferUsage: GPUBufferUsageFlags,
+ expectedData: Uint8Array
+ ): Promise<void> {
+ const mappable = bufferUsage & GPUBufferUsage.MAP_READ;
+ this.expectGPUBufferValuesEqual(buffer, expectedData, 0, { method: mappable ? 'map' : 'copy' });
+ }
+
+ TestBufferZeroInitInBindGroup(
+ computeShaderModule: GPUShaderModule,
+ buffer: GPUBuffer,
+ bufferOffset: number,
+ boundBufferSize: number
+ ): void {
+ const computePipeline = this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: computeShaderModule,
+ entryPoint: 'main',
+ },
+ });
+ const outputTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
+ });
+ this.trackForCleanup(outputTexture);
+ const bindGroup = this.device.createBindGroup({
+ layout: computePipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ offset: bufferOffset,
+ size: boundBufferSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: outputTexture.createView(),
+ },
+ ],
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const computePass = encoder.beginComputePass();
+ computePass.setBindGroup(0, bindGroup);
+ computePass.setPipeline(computePipeline);
+ computePass.dispatchWorkgroups(1);
+ computePass.end();
+ this.queue.submit([encoder.finish()]);
+
+ this.CheckBufferAndOutputTexture(buffer, boundBufferSize + bufferOffset, outputTexture);
+ }
+
+ CreateRenderPipelineForTest(
+ vertexShaderModule: GPUShaderModule,
+ testVertexBuffer: boolean
+ ): GPURenderPipeline {
+ const renderPipelineDescriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: vertexShaderModule,
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment
+ fn main(@location(0) i_color : vec4<f32>) -> @location(0) vec4<f32> {
+ return i_color;
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ };
+ if (testVertexBuffer) {
+ renderPipelineDescriptor.vertex.buffers = [
+ {
+ arrayStride: 16,
+ attributes: [{ format: 'float32x4', offset: 0, shaderLocation: 0 }],
+ },
+ ];
+ }
+
+ return this.device.createRenderPipeline(renderPipelineDescriptor);
+ }
+
+ RecordInitializeTextureColor(
+ encoder: GPUCommandEncoder,
+ texture: GPUTexture,
+ color: GPUColor
+ ): void {
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(),
+ clearValue: color,
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.end();
+ }
+
+ CheckBufferAndOutputTexture(
+ buffer: GPUBuffer,
+ bufferSize: number,
+ outputTexture: GPUTexture,
+ outputTextureSize: [number, number, number] = [1, 1, 1],
+ outputTextureColor: PerTexelComponent<number> = { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }
+ ): void {
+ this.expectSingleColor(outputTexture, 'rgba8unorm', {
+ size: outputTextureSize,
+ exp: outputTextureColor,
+ });
+
+ const expectedBufferData = new Uint8Array(bufferSize);
+ this.expectGPUBufferValuesEqual(buffer, expectedBufferData);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('partial_write_buffer')
+ .desc(
+ `Verify when we upload data to a part of a buffer with writeBuffer() just after the creation of
+the buffer, the remaining part of that buffer will be initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('offset', [0, 8, -12]))
+ .fn(async t => {
+ const { offset } = t.params;
+ const bufferSize = 32;
+ const appliedOffset = offset >= 0 ? offset : bufferSize + offset;
+
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(buffer);
+
+ const copySize = 12;
+ const writeData = new Uint8Array(copySize);
+ const expectedData = new Uint8Array(bufferSize);
+ for (let i = 0; i < copySize; ++i) {
+ expectedData[appliedOffset + i] = writeData[i] = i + 1;
+ }
+ t.queue.writeBuffer(buffer, appliedOffset, writeData, 0);
+
+ t.expectGPUBufferValuesEqual(buffer, expectedData);
+ });
+
+g.test('map_whole_buffer')
+ .desc(
+ `Verify when we map the whole range of a mappable GPUBuffer to a typed array buffer just after
+creating the GPUBuffer, the contents of both the typed array buffer and the GPUBuffer itself
+have already been initialized to 0.`
+ )
+ .params(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+
+ const bufferSize = 32;
+ const bufferUsage = t.GetBufferUsageFromMapMode(mapMode);
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ await buffer.mapAsync(mapMode);
+ const readData = new Uint8Array(buffer.getMappedRange());
+ for (let i = 0; i < bufferSize; ++i) {
+ t.expect(readData[i] === 0);
+ }
+ buffer.unmap();
+
+ const expectedData = new Uint8Array(bufferSize);
+ await t.CheckGPUBufferContent(buffer, bufferUsage, expectedData);
+ });
+
+g.test('map_partial_buffer')
+ .desc(
+ `Verify when we map a subrange of a mappable GPUBuffer to a typed array buffer just after the
+creation of the GPUBuffer, the contents of both the typed array buffer and the GPUBuffer have
+already been initialized to 0.`
+ )
+ .params(u => u.combine('mapMode', kMapModeOptions).beginSubcases().combine('offset', [0, 8, -16]))
+ .fn(async t => {
+ const { mapMode, offset } = t.params;
+ const bufferSize = 32;
+ const appliedOffset = offset >= 0 ? offset : bufferSize + offset;
+
+ const bufferUsage = t.GetBufferUsageFromMapMode(mapMode);
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ const expectedData = new Uint8Array(bufferSize);
+ {
+ const mapSize = 16;
+ await buffer.mapAsync(mapMode, appliedOffset, mapSize);
+ const mappedData = new Uint8Array(buffer.getMappedRange(appliedOffset, mapSize));
+ for (let i = 0; i < mapSize; ++i) {
+ t.expect(mappedData[i] === 0);
+ if (mapMode === GPUMapMode.WRITE) {
+ mappedData[i] = expectedData[appliedOffset + i] = i + 1;
+ }
+ }
+ buffer.unmap();
+ }
+
+ await t.CheckGPUBufferContent(buffer, bufferUsage, expectedData);
+ });
+
+g.test('mapped_at_creation_whole_buffer')
+ .desc(
+ `Verify when we call getMappedRange() at the whole range of a GPUBuffer created with
+mappedAtCreation === true just after its creation, the contents of both the returned typed
+array buffer of getMappedRange() and the GPUBuffer itself have all been initialized to 0.`
+ )
+ .params(u => u.combine('bufferUsage', kBufferUsagesForMappedAtCreationTests))
+ .fn(async t => {
+ const { bufferUsage } = t.params;
+
+ const bufferSize = 32;
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ const mapped = new Uint8Array(buffer.getMappedRange());
+ for (let i = 0; i < bufferSize; ++i) {
+ t.expect(mapped[i] === 0);
+ }
+ buffer.unmap();
+
+ const expectedData = new Uint8Array(bufferSize);
+ await t.CheckGPUBufferContent(buffer, bufferUsage, expectedData);
+ });
+
+g.test('mapped_at_creation_partial_buffer')
+ .desc(
+ `Verify when we call getMappedRange() at a subrange of a GPUBuffer created with
+mappedAtCreation === true just after its creation, the contents of both the returned typed
+array buffer of getMappedRange() and the GPUBuffer itself have all been initialized to 0.`
+ )
+ .params(u =>
+ u
+ .combine('bufferUsage', kBufferUsagesForMappedAtCreationTests)
+ .beginSubcases()
+ .combine('offset', [0, 8, -16])
+ )
+ .fn(async t => {
+ const { bufferUsage, offset } = t.params;
+ const bufferSize = 32;
+ const appliedOffset = offset >= 0 ? offset : bufferSize + offset;
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ const expectedData = new Uint8Array(bufferSize);
+ {
+ const mappedSize = 12;
+ const mapped = new Uint8Array(buffer.getMappedRange(appliedOffset, mappedSize));
+ for (let i = 0; i < mappedSize; ++i) {
+ t.expect(mapped[i] === 0);
+ if (!(bufferUsage & GPUBufferUsage.MAP_READ)) {
+ mapped[i] = expectedData[appliedOffset + i] = i + 1;
+ }
+ }
+ buffer.unmap();
+ }
+
+ await t.CheckGPUBufferContent(buffer, bufferUsage, expectedData);
+ });
+
+g.test('copy_buffer_to_buffer_copy_source')
+ .desc(
+ `Verify when the first usage of a GPUBuffer is being used as the source buffer of
+CopyBufferToBuffer(), the contents of the GPUBuffer have already been initialized to 0.`
+ )
+ .fn(async t => {
+ const bufferSize = 32;
+ const bufferUsage = GPUBufferUsage.COPY_SRC;
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ const expectedData = new Uint8Array(bufferSize);
+ // copyBufferToBuffer() is called inside t.CheckGPUBufferContent().
+ await t.CheckGPUBufferContent(buffer, bufferUsage, expectedData);
+ });
+
+g.test('copy_buffer_to_texture')
+ .desc(
+ `Verify when the first usage of a GPUBuffer is being used as the source buffer of
+CopyBufferToTexture(), the contents of the GPUBuffer have already been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 8]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+ const textureSize: [number, number, number] = [8, 8, 1];
+ const dstTextureFormat = 'rgba8unorm';
+
+ const dstTexture = t.device.createTexture({
+ size: textureSize,
+ format: dstTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+ t.trackForCleanup(dstTexture);
+ const layout = getTextureCopyLayout(dstTextureFormat, '2d', textureSize);
+ const srcBufferSize = layout.byteLength + bufferOffset;
+ const srcBufferUsage = GPUBufferUsage.COPY_SRC;
+ const srcBuffer = t.device.createBuffer({
+ size: srcBufferSize,
+ usage: srcBufferUsage,
+ });
+ t.trackForCleanup(srcBuffer);
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToTexture(
+ {
+ buffer: srcBuffer,
+ offset: bufferOffset,
+ bytesPerRow: layout.bytesPerRow,
+ rowsPerImage: layout.rowsPerImage,
+ },
+ { texture: dstTexture },
+ textureSize
+ );
+ t.queue.submit([encoder.finish()]);
+
+ t.CheckBufferAndOutputTexture(srcBuffer, srcBufferSize, dstTexture, textureSize, {
+ R: 0.0,
+ G: 0.0,
+ B: 0.0,
+ A: 0.0,
+ });
+ });
+
+g.test('resolve_query_set_to_partial_buffer')
+ .desc(
+ `Verify when we resolve a query set into a GPUBuffer just after creating that GPUBuffer, the
+remaining part of it will be initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+ const bufferSize = bufferOffset + 8;
+ const bufferUsage = GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE;
+ const dstBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(dstBuffer);
+
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: 1 });
+ const encoder = t.device.createCommandEncoder();
+ encoder.resolveQuerySet(querySet, 0, 1, dstBuffer, bufferOffset);
+ t.queue.submit([encoder.finish()]);
+
+ const expectedBufferData = new Uint8Array(bufferSize);
+ await t.CheckGPUBufferContent(dstBuffer, bufferUsage, expectedBufferData);
+ });
+
+g.test('copy_texture_to_partial_buffer')
+ .desc(
+ `Verify when we copy from a GPUTexture into a GPUBuffer just after creating that GPUBuffer, the
+remaining part of it will be initialized to 0.`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('bufferOffset', [0, 8, -16])
+ .combine('arrayLayerCount', [1, 3])
+ .combine('copyMipLevel', [0, 2])
+ .combine('rowsPerImage', [16, 20])
+ .filter(t => {
+ // We don't need to test the copies that will cover the whole GPUBuffer.
+ return !(t.bufferOffset === 0 && t.rowsPerImage === 16);
+ })
+ )
+ .fn(async t => {
+ const { bufferOffset, arrayLayerCount, copyMipLevel, rowsPerImage } = t.params;
+ const srcTextureFormat = 'r8uint';
+ const textureSize = [32, 16, arrayLayerCount] as const;
+
+ const srcTexture = t.device.createTexture({
+ format: srcTextureFormat,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ size: textureSize,
+ mipLevelCount: copyMipLevel + 1,
+ });
+ t.trackForCleanup(srcTexture);
+
+ const bytesPerRow = 256;
+ const layout = getTextureCopyLayout(srcTextureFormat, '2d', textureSize, {
+ mipLevel: copyMipLevel,
+ bytesPerRow,
+ rowsPerImage,
+ });
+
+ const dstBufferSize = layout.byteLength + Math.abs(bufferOffset);
+ const dstBuffer = t.device.createBuffer({
+ size: dstBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(dstBuffer);
+
+ const encoder = t.device.createCommandEncoder();
+
+ // Initialize srcTexture
+ for (let layer = 0; layer < arrayLayerCount; ++layer) {
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: srcTexture.createView({
+ baseArrayLayer: layer,
+ arrayLayerCount: 1,
+ baseMipLevel: copyMipLevel,
+ }),
+ clearValue: { r: layer + 1, g: 0, b: 0, a: 0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.end();
+ }
+
+ // Do texture-to-buffer copy
+ const appliedOffset = Math.max(bufferOffset, 0);
+ encoder.copyTextureToBuffer(
+ { texture: srcTexture, mipLevel: copyMipLevel },
+ { buffer: dstBuffer, offset: appliedOffset, bytesPerRow, rowsPerImage },
+ layout.mipSize
+ );
+ t.queue.submit([encoder.finish()]);
+
+ // Check if the contents of the destination buffer are what we expect.
+ const expectedData = new Uint8Array(dstBufferSize);
+ for (let layer = 0; layer < arrayLayerCount; ++layer) {
+ for (let y = 0; y < layout.mipSize[1]; ++y) {
+ for (let x = 0; x < layout.mipSize[0]; ++x) {
+ expectedData[appliedOffset + layer * bytesPerRow * rowsPerImage + y * bytesPerRow + x] =
+ layer + 1;
+ }
+ }
+ }
+ t.expectGPUBufferValuesEqual(dstBuffer, expectedData);
+ });
+
+g.test('uniform_buffer')
+ .desc(
+ `Verify when we use a GPUBuffer as a uniform buffer just after the creation of that GPUBuffer,
+ all the contents in that GPUBuffer have been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+
+ const boundBufferSize = 16;
+ const buffer = t.device.createBuffer({
+ size: bufferOffset + boundBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.UNIFORM,
+ });
+ t.trackForCleanup(buffer);
+
+ const computeShaderModule = t.device.createShaderModule({
+ code: `
+ struct UBO {
+ value : vec4<u32>
+ };
+ @group(0) @binding(0) var<uniform> ubo : UBO;
+ @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(1) fn main() {
+ if (all(ubo.value == vec4<u32>(0u, 0u, 0u, 0u))) {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0));
+ } else {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0));
+ }
+ }`,
+ });
+
+ // Verify the whole range of the buffer has been initialized to 0 in a compute shader.
+ t.TestBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize);
+ });
+
+g.test('readonly_storage_buffer')
+ .desc(
+ `Verify when we use a GPUBuffer as a read-only storage buffer just after the creation of that
+ GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+ const boundBufferSize = 16;
+ const buffer = t.device.createBuffer({
+ size: bufferOffset + boundBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+ t.trackForCleanup(buffer);
+
+ const computeShaderModule = t.device.createShaderModule({
+ code: `
+ struct SSBO {
+ value : vec4<u32>
+ };
+ @group(0) @binding(0) var<storage, read> ssbo : SSBO;
+ @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(1) fn main() {
+ if (all(ssbo.value == vec4<u32>(0u, 0u, 0u, 0u))) {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0));
+ } else {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0));
+ }
+ }`,
+ });
+
+ // Verify the whole range of the buffer has been initialized to 0 in a compute shader.
+ t.TestBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize);
+ });
+
+g.test('storage_buffer')
+ .desc(
+ `Verify when we use a GPUBuffer as a storage buffer just after the creation of that
+ GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+ const boundBufferSize = 16;
+ const buffer = t.device.createBuffer({
+ size: bufferOffset + boundBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+ t.trackForCleanup(buffer);
+
+ const computeShaderModule = t.device.createShaderModule({
+ code: `
+ struct SSBO {
+ value : vec4<u32>
+ };
+ @group(0) @binding(0) var<storage, read_write> ssbo : SSBO;
+ @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(1) fn main() {
+ if (all(ssbo.value == vec4<u32>(0u, 0u, 0u, 0u))) {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0));
+ } else {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0));
+ }
+ }`,
+ });
+
+ // Verify the whole range of the buffer has been initialized to 0 in a compute shader.
+ t.TestBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize);
+ });
+
+g.test('vertex_buffer')
+ .desc(
+ `Verify when we use a GPUBuffer as a vertex buffer just after the creation of that
+ GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+
+ const renderPipeline = t.CreateRenderPipelineForTest(
+ t.device.createShaderModule({
+ code: `
+ struct VertexOut {
+ @location(0) color : vec4<f32>,
+ @builtin(position) position : vec4<f32>,
+ };
+
+ @vertex fn main(@location(0) pos : vec4<f32>) -> VertexOut {
+ var output : VertexOut;
+ if (all(pos == vec4<f32>(0.0, 0.0, 0.0, 0.0))) {
+ output.color = vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ } else {
+ output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ return output;
+ }`,
+ }),
+ true
+ );
+
+ const bufferSize = 16 + bufferOffset;
+ const vertexBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(vertexBuffer);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(outputTexture);
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setVertexBuffer(0, vertexBuffer, bufferOffset);
+ renderPass.setPipeline(renderPipeline);
+ renderPass.draw(1);
+ renderPass.end();
+ t.queue.submit([encoder.finish()]);
+
+ t.CheckBufferAndOutputTexture(vertexBuffer, bufferSize, outputTexture);
+ });
+
+g.test('index_buffer')
+ .desc(
+ `Verify when we use a GPUBuffer as an index buffer just after the creation of that
+GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+
+ const renderPipeline = t.CreateRenderPipelineForTest(
+ t.device.createShaderModule({
+ code: `
+ struct VertexOut {
+ @location(0) color : vec4<f32>,
+ @builtin(position) position : vec4<f32>,
+ };
+
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOut {
+ var output : VertexOut;
+ if (VertexIndex == 0u) {
+ output.color = vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ } else {
+ output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }
+ output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ return output;
+ }`,
+ }),
+ false
+ );
+
+ // The size of GPUBuffer must be at least 4.
+ const bufferSize = 4 + bufferOffset;
+ const indexBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(indexBuffer);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(outputTexture);
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(renderPipeline);
+ renderPass.setIndexBuffer(indexBuffer, 'uint16', bufferOffset, 4);
+ renderPass.drawIndexed(1);
+ renderPass.end();
+ t.queue.submit([encoder.finish()]);
+
+ t.CheckBufferAndOutputTexture(indexBuffer, bufferSize, outputTexture);
+ });
+
+g.test('indirect_buffer_for_draw_indirect')
+ .desc(
+ `Verify when we use a GPUBuffer as an indirect buffer for drawIndirect() or
+drawIndexedIndirect() just after the creation of that GPUBuffer, all the contents in that GPUBuffer
+have been initialized to 0.`
+ )
+ .params(u =>
+ u.combine('test_indexed_draw', [true, false]).beginSubcases().combine('bufferOffset', [0, 16])
+ )
+ .fn(async t => {
+ const { test_indexed_draw, bufferOffset } = t.params;
+
+ const renderPipeline = t.CreateRenderPipelineForTest(
+ t.device.createShaderModule({
+ code: `
+ struct VertexOut {
+ @location(0) color : vec4<f32>,
+ @builtin(position) position : vec4<f32>,
+ };
+
+ @vertex fn main() -> VertexOut {
+ var output : VertexOut;
+ output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ return output;
+ }`,
+ }),
+ false
+ );
+
+ const kDrawIndirectParametersSize = 16;
+ const kDrawIndexedIndirectParametersSize = 20;
+ const bufferSize =
+ Math.max(kDrawIndirectParametersSize, kDrawIndexedIndirectParametersSize) + bufferOffset;
+ const indirectBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.INDIRECT,
+ });
+ t.trackForCleanup(indirectBuffer);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(outputTexture);
+
+ // Initialize outputTexture to green.
+ const encoder = t.device.createCommandEncoder();
+ t.RecordInitializeTextureColor(encoder, outputTexture, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 });
+
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPass.setPipeline(renderPipeline);
+
+ let indexBuffer = undefined;
+ if (test_indexed_draw) {
+ indexBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ t.trackForCleanup(indexBuffer);
+ renderPass.setIndexBuffer(indexBuffer, 'uint16');
+ renderPass.drawIndexedIndirect(indirectBuffer, bufferOffset);
+ } else {
+ renderPass.drawIndirect(indirectBuffer, bufferOffset);
+ }
+
+ renderPass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // The indirect buffer should be lazily cleared to 0, so we actually draw nothing and the color
+ // attachment will keep its original color (green) after we end the render pass.
+ t.CheckBufferAndOutputTexture(indirectBuffer, bufferSize, outputTexture);
+ });
+
+g.test('indirect_buffer_for_dispatch_indirect')
+ .desc(
+ `Verify when we use a GPUBuffer as an indirect buffer for dispatchWorkgroupsIndirect() just
+ after the creation of that GPUBuffer, all the contents in that GPUBuffer have been initialized
+ to 0.`
+ )
+ .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16]))
+ .fn(async t => {
+ const { bufferOffset } = t.params;
+
+ const computePipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var outImage : texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(1) fn main() {
+ textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0));
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const kDispatchIndirectParametersSize = 12;
+ const bufferSize = kDispatchIndirectParametersSize + bufferOffset;
+ const indirectBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.INDIRECT,
+ });
+ t.trackForCleanup(indirectBuffer);
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ GPUTextureUsage.RENDER_ATTACHMENT |
+ GPUTextureUsage.STORAGE_BINDING,
+ });
+ t.trackForCleanup(outputTexture);
+
+ // Initialize outputTexture to green.
+ const encoder = t.device.createCommandEncoder();
+ t.RecordInitializeTextureColor(encoder, outputTexture, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: computePipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: outputTexture.createView(),
+ },
+ ],
+ });
+
+ // The indirect buffer should be lazily cleared to 0, so we actually don't execute the compute
+ // shader and the output texture should keep its original color (green).
+ const computePass = encoder.beginComputePass();
+ computePass.setBindGroup(0, bindGroup);
+ computePass.setPipeline(computePipeline);
+ computePass.dispatchWorkgroupsIndirect(indirectBuffer, bufferOffset);
+ computePass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // The indirect buffer should be lazily cleared to 0, so we actually draw nothing and the color
+ // attachment will keep its original color (green) after we end the compute pass.
+ t.CheckBufferAndOutputTexture(indirectBuffer, bufferSize, outputTexture);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts
new file mode 100644
index 0000000000..8f835e0f85
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts
@@ -0,0 +1,66 @@
+import { assert } from '../../../../../common/util/util.js';
+import { EncodableTextureFormat, kTextureFormatInfo } from '../../../../capability_info.js';
+import { virtualMipSize } from '../../../../util/texture/base.js';
+import { CheckContents } from '../texture_zero.spec.js';
+
+export const checkContentsByBufferCopy: CheckContents = (
+ t,
+ params,
+ texture,
+ state,
+ subresourceRange
+) => {
+ for (const { level: mipLevel, layer } of subresourceRange.each()) {
+ assert(params.format in kTextureFormatInfo);
+ const format = params.format as EncodableTextureFormat;
+
+ t.expectSingleColor(texture, format, {
+ size: [t.textureWidth, t.textureHeight, t.textureDepth],
+ dimension: params.dimension,
+ slice: layer,
+ layout: { mipLevel, aspect: params.aspect },
+ exp: t.stateToTexelComponents[state],
+ });
+ }
+};
+
+export const checkContentsByTextureCopy: CheckContents = (
+ t,
+ params,
+ texture,
+ state,
+ subresourceRange
+) => {
+ for (const { level, layer } of subresourceRange.each()) {
+ assert(params.format in kTextureFormatInfo);
+ const format = params.format as EncodableTextureFormat;
+
+ const [width, height, depth] = virtualMipSize(
+ params.dimension,
+ [t.textureWidth, t.textureHeight, t.textureDepth],
+ level
+ );
+
+ const dst = t.device.createTexture({
+ dimension: params.dimension,
+ size: [width, height, depth],
+ format: params.format,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ });
+ t.trackForCleanup(dst);
+
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.copyTextureToTexture(
+ { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } },
+ { texture: dst, mipLevel: 0 },
+ { width, height, depthOrArrayLayers: depth }
+ );
+ t.queue.submit([commandEncoder.finish()]);
+
+ t.expectSingleColor(dst, format, {
+ size: [width, height, depth],
+ exp: t.stateToTexelComponents[state],
+ layout: { mipLevel: 0, aspect: params.aspect },
+ });
+ }
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts
new file mode 100644
index 0000000000..1851945e42
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts
@@ -0,0 +1,197 @@
+import { assert } from '../../../../../common/util/util.js';
+import { kTextureFormatInfo } from '../../../../capability_info.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { virtualMipSize } from '../../../../util/texture/base.js';
+import { CheckContents } from '../texture_zero.spec.js';
+
+function makeFullscreenVertexModule(device: GPUDevice) {
+ return device.createShaderModule({
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)
+ -> @builtin(position) vec4<f32> {
+ var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -3.0),
+ vec2<f32>( 3.0, 1.0),
+ vec2<f32>(-1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+ `,
+ });
+}
+
+function getDepthTestEqualPipeline(
+ t: GPUTest,
+ format: GPUTextureFormat,
+ sampleCount: number,
+ expected: number
+): GPURenderPipeline {
+ return t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ entryPoint: 'main',
+ module: makeFullscreenVertexModule(t.device),
+ },
+ fragment: {
+ entryPoint: 'main',
+ module: t.device.createShaderModule({
+ code: `
+ struct Outputs {
+ @builtin(frag_depth) FragDepth : f32,
+ @location(0) outSuccess : f32,
+ };
+
+ @fragment
+ fn main() -> Outputs {
+ var output : Outputs;
+ output.FragDepth = f32(${expected});
+ output.outSuccess = 1.0;
+ return output;
+ }
+ `,
+ }),
+ targets: [{ format: 'r8unorm' }],
+ },
+ depthStencil: {
+ format,
+ depthCompare: 'equal',
+ },
+ primitive: { topology: 'triangle-list' },
+ multisample: { count: sampleCount },
+ });
+}
+
+function getStencilTestEqualPipeline(
+ t: GPUTest,
+ format: GPUTextureFormat,
+ sampleCount: number
+): GPURenderPipeline {
+ return t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ entryPoint: 'main',
+ module: makeFullscreenVertexModule(t.device),
+ },
+ fragment: {
+ entryPoint: 'main',
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) f32 {
+ return 1.0;
+ }
+ `,
+ }),
+ targets: [{ format: 'r8unorm' }],
+ },
+ depthStencil: {
+ format,
+ stencilFront: { compare: 'equal' },
+ stencilBack: { compare: 'equal' },
+ },
+ primitive: { topology: 'triangle-list' },
+ multisample: { count: sampleCount },
+ });
+}
+
+const checkContents: (type: 'depth' | 'stencil', ...args: Parameters<CheckContents>) => void = (
+ type,
+ t,
+ params,
+ texture,
+ state,
+ subresourceRange
+) => {
+ const formatInfo = kTextureFormatInfo[params.format];
+
+ assert(params.dimension === '2d');
+ for (const viewDescriptor of t.generateTextureViewDescriptorsForRendering(
+ 'all',
+ subresourceRange
+ )) {
+ assert(viewDescriptor.baseMipLevel !== undefined);
+ const [width, height] = virtualMipSize(
+ params.dimension,
+ [t.textureWidth, t.textureHeight, 1],
+ viewDescriptor.baseMipLevel
+ );
+
+ const renderTexture = t.device.createTexture({
+ size: [width, height, 1],
+ format: 'r8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ sampleCount: params.sampleCount,
+ });
+
+ let resolveTexture = undefined;
+ let resolveTarget = undefined;
+ if (params.sampleCount > 1) {
+ resolveTexture = t.device.createTexture({
+ size: [width, height, 1],
+ format: 'r8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ resolveTarget = resolveTexture.createView();
+ }
+
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.pushDebugGroup('checkContentsWithDepthStencil');
+
+ const pass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTexture.createView(),
+ resolveTarget,
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: texture.createView(viewDescriptor),
+ depthStoreOp: formatInfo.depth ? 'store' : undefined,
+ depthLoadOp: formatInfo.depth ? 'load' : undefined,
+ stencilStoreOp: formatInfo.stencil ? 'store' : undefined,
+ stencilLoadOp: formatInfo.stencil ? 'load' : undefined,
+ },
+ });
+
+ switch (type) {
+ case 'depth': {
+ const expectedDepth = t.stateToTexelComponents[state].Depth;
+ assert(expectedDepth !== undefined);
+
+ pass.setPipeline(
+ getDepthTestEqualPipeline(t, params.format, params.sampleCount, expectedDepth)
+ );
+ break;
+ }
+
+ case 'stencil': {
+ const expectedStencil = t.stateToTexelComponents[state].Stencil;
+ assert(expectedStencil !== undefined);
+
+ pass.setPipeline(getStencilTestEqualPipeline(t, params.format, params.sampleCount));
+ pass.setStencilReference(expectedStencil);
+ break;
+ }
+ }
+
+ pass.draw(3);
+ pass.end();
+
+ commandEncoder.popDebugGroup();
+ t.queue.submit([commandEncoder.finish()]);
+
+ t.expectSingleColor(resolveTexture || renderTexture, 'r8unorm', {
+ size: [width, height, 1],
+ exp: { R: 1 },
+ });
+ }
+};
+
+export const checkContentsByDepthTest = (...args: Parameters<CheckContents>) =>
+ checkContents('depth', ...args);
+
+export const checkContentsByStencilTest = (...args: Parameters<CheckContents>) =>
+ checkContents('stencil', ...args);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts
new file mode 100644
index 0000000000..f739c128dc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts
@@ -0,0 +1,157 @@
+import { assert, unreachable } from '../../../../../common/util/util.js';
+import { EncodableTextureFormat, kTextureFormatInfo } from '../../../../capability_info.js';
+import { virtualMipSize } from '../../../../util/texture/base.js';
+import {
+ kTexelRepresentationInfo,
+ getSingleDataType,
+ getComponentReadbackTraits,
+} from '../../../../util/texture/texel_data.js';
+import { CheckContents } from '../texture_zero.spec.js';
+
+export const checkContentsBySampling: CheckContents = (
+ t,
+ params,
+ texture,
+ state,
+ subresourceRange
+) => {
+ assert(params.format in kTextureFormatInfo);
+ const format = params.format as EncodableTextureFormat;
+ const rep = kTexelRepresentationInfo[format];
+
+ for (const { level, layers } of subresourceRange.mipLevels()) {
+ const [width, height, depth] = virtualMipSize(
+ params.dimension,
+ [t.textureWidth, t.textureHeight, t.textureDepth],
+ level
+ );
+
+ const { ReadbackTypedArray, shaderType } = getComponentReadbackTraits(
+ getSingleDataType(format)
+ );
+
+ const componentOrder = rep.componentOrder;
+ const componentCount = componentOrder.length;
+
+ // For single-component textures, generates .r
+ // For multi-component textures, generates ex.)
+ // .rgba[i], .bgra[i], .rgb[i]
+ const indexExpression =
+ componentCount === 1
+ ? componentOrder[0].toLowerCase()
+ : componentOrder.map(c => c.toLowerCase()).join('') + '[i]';
+
+ const _xd = '_' + params.dimension;
+ const _multisampled = params.sampleCount > 1 ? '_multisampled' : '';
+ const texelIndexExpression =
+ params.dimension === '2d'
+ ? 'vec2<i32>(GlobalInvocationID.xy)'
+ : params.dimension === '3d'
+ ? 'vec3<i32>(GlobalInvocationID.xyz)'
+ : params.dimension === '1d'
+ ? 'i32(GlobalInvocationID.x)'
+ : unreachable();
+ const computePipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ entryPoint: 'main',
+ module: t.device.createShaderModule({
+ code: `
+ struct Constants {
+ level : i32
+ };
+
+ @group(0) @binding(0) var<uniform> constants : Constants;
+ @group(0) @binding(1) var myTexture : texture${_multisampled}${_xd}<${shaderType}>;
+
+ struct Result {
+ values : array<${shaderType}>
+ };
+ @group(0) @binding(3) var<storage, read_write> result : Result;
+
+ @compute @workgroup_size(1)
+ fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ let flatIndex : u32 = ${componentCount}u * (
+ ${width}u * ${height}u * GlobalInvocationID.z +
+ ${width}u * GlobalInvocationID.y +
+ GlobalInvocationID.x
+ );
+ let texel : vec4<${shaderType}> = textureLoad(
+ myTexture, ${texelIndexExpression}, constants.level);
+
+ for (var i : u32 = 0u; i < ${componentCount}u; i = i + 1u) {
+ result.values[flatIndex + i] = texel.${indexExpression};
+ }
+ }`,
+ }),
+ },
+ });
+
+ for (const layer of layers) {
+ const ubo = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: 4,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ new Int32Array(ubo.getMappedRange(), 0, 1)[0] = level;
+ ubo.unmap();
+
+ const byteLength =
+ width * height * depth * ReadbackTypedArray.BYTES_PER_ELEMENT * rep.componentOrder.length;
+ const resultBuffer = t.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(resultBuffer);
+
+ const bindGroup = t.device.createBindGroup({
+ layout: computePipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: ubo },
+ },
+ {
+ binding: 1,
+ resource: texture.createView({
+ baseArrayLayer: layer,
+ arrayLayerCount: 1,
+ dimension: params.dimension,
+ }),
+ },
+ {
+ binding: 3,
+ resource: {
+ buffer: resultBuffer,
+ },
+ },
+ ],
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const pass = commandEncoder.beginComputePass();
+ pass.setPipeline(computePipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(width, height, depth);
+ pass.end();
+ t.queue.submit([commandEncoder.finish()]);
+ ubo.destroy();
+
+ const expectedValues = new ReadbackTypedArray(new ArrayBuffer(byteLength));
+ const expectedState = t.stateToTexelComponents[state];
+ let i = 0;
+ for (let d = 0; d < depth; ++d) {
+ for (let h = 0; h < height; ++h) {
+ for (let w = 0; w < width; ++w) {
+ for (const c of rep.componentOrder) {
+ const value = expectedState[c];
+ assert(value !== undefined);
+ expectedValues[i++] = value;
+ }
+ }
+ }
+ }
+ t.expectGPUBufferValuesEqual(resultBuffer, expectedValues);
+ }
+ }
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts
new file mode 100644
index 0000000000..cdb383ad65
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/resource_init/texture_zero.spec.ts
@@ -0,0 +1,645 @@
+export const description = `
+Test uninitialized textures are initialized to zero when read.
+
+TODO:
+- test by sampling depth/stencil [1]
+- test by copying out of stencil [2]
+- test compressed texture formats [3]
+`;
+
+// MAINTENANCE_TODO: This is a test file, it probably shouldn't export anything.
+// Everything that's exported should be moved to another file.
+
+import { TestCaseRecorder, TestParams } from '../../../../common/framework/fixture.js';
+import {
+ kUnitCaseParamsBuilder,
+ ParamTypeOf,
+} from '../../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ kTextureAspects,
+ kUncompressedTextureFormats,
+ EncodableTextureFormat,
+ UncompressedTextureFormat,
+ textureDimensionAndFormatCompatible,
+ kTextureDimensions,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { GPUTest, GPUTestSubcaseBatchState } from '../../../gpu_test.js';
+import { virtualMipSize } from '../../../util/texture/base.js';
+import { createTextureUploadBuffer } from '../../../util/texture/layout.js';
+import { BeginEndRange, SubresourceRange } from '../../../util/texture/subresource.js';
+import { PerTexelComponent, kTexelRepresentationInfo } from '../../../util/texture/texel_data.js';
+
+export enum UninitializeMethod {
+ Creation = 'Creation', // The texture was just created. It is uninitialized.
+ StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear"
+}
+const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[];
+
+export const enum ReadMethod {
+ Sample = 'Sample', // The texture is sampled from
+ CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer
+ CopyToTexture = 'CopyToTexture', // The texture is copied to another texture
+ DepthTest = 'DepthTest', // The texture is read as a depth buffer
+ StencilTest = 'StencilTest', // The texture is read as a stencil buffer
+ ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment
+ Storage = 'Storage', // Read the texture as a storage texture
+}
+
+// Test with these mip level counts
+type MipLevels = 1 | 5;
+const kMipLevelCounts: MipLevels[] = [1, 5];
+
+// For each mip level count, define the mip ranges to leave uninitialized.
+const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = {
+ 1: [{ begin: 0, end: 1 }], // Test the only mip
+ 5: [
+ { begin: 0, end: 2 },
+ { begin: 3, end: 4 },
+ ], // Test a range and a single mip
+};
+
+// Test with these sample counts.
+const kSampleCounts: number[] = [1, 4];
+
+// Test with these layer counts.
+type LayerCounts = 1 | 7;
+
+// For each layer count, define the layers to leave uninitialized.
+const kUninitializedLayerRangesToTest: { [k in LayerCounts]: BeginEndRange[] } = {
+ 1: [{ begin: 0, end: 1 }], // Test the only layer
+ 7: [
+ { begin: 2, end: 4 },
+ { begin: 6, end: 7 },
+ ], // Test a range and a single layer
+};
+
+// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format,
+// the data for each value may have a different representation. These enums are converted to a
+// representation such that their values can be compared. ex.) An integer is needed to upload to an
+// unsigned normalized format, but its value is read as a float in the shader.
+export const enum InitializedState {
+ Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero.
+ Zero, // We check that uninitialized subresources are in this state when read back.
+}
+
+const initializedStateAsFloat = {
+ [InitializedState.Zero]: 0,
+ [InitializedState.Canary]: 1,
+};
+
+const initializedStateAsUint = {
+ [InitializedState.Zero]: 0,
+ [InitializedState.Canary]: 1,
+};
+
+const initializedStateAsSint = {
+ [InitializedState.Zero]: 0,
+ [InitializedState.Canary]: -1,
+};
+
+function initializedStateAsColor(
+ state: InitializedState,
+ format: GPUTextureFormat
+): [number, number, number, number] {
+ let value;
+ if (format.indexOf('uint') !== -1) {
+ value = initializedStateAsUint[state];
+ } else if (format.indexOf('sint') !== -1) {
+ value = initializedStateAsSint[state];
+ } else {
+ value = initializedStateAsFloat[state];
+ }
+ return [value, value, value, value];
+}
+
+const initializedStateAsDepth = {
+ [InitializedState.Zero]: 0,
+ [InitializedState.Canary]: 0.8,
+};
+
+const initializedStateAsStencil = {
+ [InitializedState.Zero]: 0,
+ [InitializedState.Canary]: 42,
+};
+
+function getRequiredTextureUsage(
+ format: UncompressedTextureFormat,
+ sampleCount: number,
+ uninitializeMethod: UninitializeMethod,
+ readMethod: ReadMethod
+): GPUTextureUsageFlags {
+ let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST;
+
+ switch (uninitializeMethod) {
+ case UninitializeMethod.Creation:
+ break;
+ case UninitializeMethod.StoreOpClear:
+ usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT;
+ break;
+ default:
+ unreachable();
+ }
+
+ switch (readMethod) {
+ case ReadMethod.CopyToBuffer:
+ case ReadMethod.CopyToTexture:
+ usage |= GPUConst.TextureUsage.COPY_SRC;
+ break;
+ case ReadMethod.Sample:
+ usage |= GPUConst.TextureUsage.TEXTURE_BINDING;
+ break;
+ case ReadMethod.Storage:
+ usage |= GPUConst.TextureUsage.STORAGE_BINDING;
+ break;
+ case ReadMethod.DepthTest:
+ case ReadMethod.StencilTest:
+ case ReadMethod.ColorBlending:
+ usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT;
+ break;
+ default:
+ unreachable();
+ }
+
+ if (sampleCount > 1) {
+ // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize
+ // canary data in multisampled textures.
+ usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT;
+ }
+
+ if (!kTextureFormatInfo[format].copyDst) {
+ // Copies are not possible. We need OutputAttachment to initialize
+ // canary data.
+ assert(kTextureFormatInfo[format].renderable);
+ usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT;
+ }
+
+ return usage;
+}
+
+export class TextureZeroInitTest extends GPUTest {
+ readonly stateToTexelComponents: { [k in InitializedState]: PerTexelComponent<number> };
+
+ private p: TextureZeroParams;
+ constructor(sharedState: GPUTestSubcaseBatchState, rec: TestCaseRecorder, params: TestParams) {
+ super(sharedState, rec, params);
+ this.p = params as TextureZeroParams;
+
+ const stateToTexelComponents = (state: InitializedState) => {
+ const [R, G, B, A] = initializedStateAsColor(state, this.p.format);
+ return {
+ R,
+ G,
+ B,
+ A,
+ Depth: initializedStateAsDepth[state],
+ Stencil: initializedStateAsStencil[state],
+ };
+ };
+
+ this.stateToTexelComponents = {
+ [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero),
+ [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary),
+ };
+ }
+
+ get textureWidth(): number {
+ let width = 1 << this.p.mipLevelCount;
+ if (this.p.nonPowerOfTwo) {
+ width = 2 * width - 1;
+ }
+ return width;
+ }
+
+ get textureHeight(): number {
+ if (this.p.dimension === '1d') {
+ return 1;
+ }
+
+ let height = 1 << this.p.mipLevelCount;
+ if (this.p.nonPowerOfTwo) {
+ height = 2 * height - 1;
+ }
+ return height;
+ }
+
+ get textureDepth(): number {
+ return this.p.dimension === '3d' ? 11 : 1;
+ }
+
+ get textureDepthOrArrayLayers(): number {
+ return this.p.dimension === '2d' ? this.p.layerCount : this.textureDepth;
+ }
+
+ // Used to iterate subresources and check that their uninitialized contents are zero when accessed
+ *iterateUninitializedSubresources(): Generator<SubresourceRange> {
+ for (const mipRange of kUninitializedMipRangesToTest[this.p.mipLevelCount]) {
+ for (const layerRange of kUninitializedLayerRangesToTest[this.p.layerCount]) {
+ yield new SubresourceRange({ mipRange, layerRange });
+ }
+ }
+ }
+
+ // Used to iterate and initialize other subresources not checked for zero-initialization.
+ // Zero-initialization of uninitialized subresources should not have side effects on already
+ // initialized subresources.
+ *iterateInitializedSubresources(): Generator<SubresourceRange> {
+ const uninitialized: boolean[][] = new Array(this.p.mipLevelCount);
+ for (let level = 0; level < uninitialized.length; ++level) {
+ uninitialized[level] = new Array(this.p.layerCount);
+ }
+ for (const subresources of this.iterateUninitializedSubresources()) {
+ for (const { level, layer } of subresources.each()) {
+ uninitialized[level][layer] = true;
+ }
+ }
+ for (let level = 0; level < uninitialized.length; ++level) {
+ for (let layer = 0; layer < uninitialized[level].length; ++layer) {
+ if (!uninitialized[level][layer]) {
+ yield new SubresourceRange({
+ mipRange: { begin: level, count: 1 },
+ layerRange: { begin: layer, count: 1 },
+ });
+ }
+ }
+ }
+ }
+
+ *generateTextureViewDescriptorsForRendering(
+ aspect: GPUTextureAspect,
+ subresourceRange?: SubresourceRange
+ ): Generator<GPUTextureViewDescriptor> {
+ const viewDescriptor: GPUTextureViewDescriptor = {
+ dimension: '2d',
+ aspect,
+ };
+
+ if (subresourceRange === undefined) {
+ return viewDescriptor;
+ }
+
+ for (const { level, layer } of subresourceRange.each()) {
+ yield {
+ ...viewDescriptor,
+ baseMipLevel: level,
+ mipLevelCount: 1,
+ baseArrayLayer: layer,
+ arrayLayerCount: 1,
+ };
+ }
+ }
+
+ private initializeWithStoreOp(
+ state: InitializedState,
+ texture: GPUTexture,
+ subresourceRange?: SubresourceRange
+ ): void {
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.pushDebugGroup('initializeWithStoreOp');
+
+ for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering(
+ 'all',
+ subresourceRange
+ )) {
+ if (kTextureFormatInfo[this.p.format].color) {
+ commandEncoder
+ .beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(viewDescriptor),
+ storeOp: 'store',
+ clearValue: initializedStateAsColor(state, this.p.format),
+ loadOp: 'clear',
+ },
+ ],
+ })
+ .end();
+ } else {
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: texture.createView(viewDescriptor),
+ };
+ if (kTextureFormatInfo[this.p.format].depth) {
+ depthStencilAttachment.depthClearValue = initializedStateAsDepth[state];
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'store';
+ }
+ if (kTextureFormatInfo[this.p.format].stencil) {
+ depthStencilAttachment.stencilClearValue = initializedStateAsStencil[state];
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+ commandEncoder
+ .beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment,
+ })
+ .end();
+ }
+ }
+
+ commandEncoder.popDebugGroup();
+ this.queue.submit([commandEncoder.finish()]);
+ }
+
+ private initializeWithCopy(
+ texture: GPUTexture,
+ state: InitializedState,
+ subresourceRange: SubresourceRange
+ ): void {
+ assert(this.p.format in kTextureFormatInfo);
+ const format = this.p.format as EncodableTextureFormat;
+
+ const firstSubresource = subresourceRange.each().next().value;
+ assert(typeof firstSubresource !== 'undefined');
+
+ const [largestWidth, largestHeight, largestDepth] = virtualMipSize(
+ this.p.dimension,
+ [this.textureWidth, this.textureHeight, this.textureDepth],
+ firstSubresource.level
+ );
+
+ const rep = kTexelRepresentationInfo[format];
+ const texelData = new Uint8Array(rep.pack(rep.encode(this.stateToTexelComponents[state])));
+ const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer(
+ texelData,
+ this.device,
+ format,
+ this.p.dimension,
+ [largestWidth, largestHeight, largestDepth]
+ );
+
+ const commandEncoder = this.device.createCommandEncoder();
+
+ for (const { level, layer } of subresourceRange.each()) {
+ const [width, height, depth] = virtualMipSize(
+ this.p.dimension,
+ [this.textureWidth, this.textureHeight, this.textureDepth],
+ level
+ );
+
+ commandEncoder.copyBufferToTexture(
+ {
+ buffer,
+ bytesPerRow,
+ rowsPerImage,
+ },
+ { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } },
+ { width, height, depthOrArrayLayers: depth }
+ );
+ }
+ this.queue.submit([commandEncoder.finish()]);
+ buffer.destroy();
+ }
+
+ initializeTexture(
+ texture: GPUTexture,
+ state: InitializedState,
+ subresourceRange: SubresourceRange
+ ): void {
+ if (this.p.sampleCount > 1 || !kTextureFormatInfo[this.p.format].copyDst) {
+ // Copies to multisampled textures not yet specified.
+ // Use a storeOp for now.
+ assert(kTextureFormatInfo[this.p.format].renderable);
+ this.initializeWithStoreOp(state, texture, subresourceRange);
+ } else {
+ this.initializeWithCopy(texture, state, subresourceRange);
+ }
+ }
+
+ discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void {
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.pushDebugGroup('discardTexture');
+
+ for (const desc of this.generateTextureViewDescriptorsForRendering('all', subresourceRange)) {
+ if (kTextureFormatInfo[this.p.format].color) {
+ commandEncoder
+ .beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(desc),
+ storeOp: 'discard',
+ loadOp: 'load',
+ },
+ ],
+ })
+ .end();
+ } else {
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: texture.createView(desc),
+ };
+ if (kTextureFormatInfo[this.p.format].depth) {
+ depthStencilAttachment.depthLoadOp = 'load';
+ depthStencilAttachment.depthStoreOp = 'discard';
+ }
+ if (kTextureFormatInfo[this.p.format].stencil) {
+ depthStencilAttachment.stencilLoadOp = 'load';
+ depthStencilAttachment.stencilStoreOp = 'discard';
+ }
+ commandEncoder
+ .beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment,
+ })
+ .end();
+ }
+ }
+
+ commandEncoder.popDebugGroup();
+ this.queue.submit([commandEncoder.finish()]);
+ }
+}
+
+const kTestParams = kUnitCaseParamsBuilder
+ .combine('dimension', kTextureDimensions)
+ .combine('readMethod', [
+ ReadMethod.CopyToBuffer,
+ ReadMethod.CopyToTexture,
+ ReadMethod.Sample,
+ ReadMethod.DepthTest,
+ ReadMethod.StencilTest,
+ ])
+ // [3] compressed formats
+ .combine('format', kUncompressedTextureFormats)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('aspect', kTextureAspects)
+ .unless(({ readMethod, format, aspect }) => {
+ const info = kTextureFormatInfo[format];
+ return (
+ (readMethod === ReadMethod.DepthTest && (!info.depth || aspect === 'stencil-only')) ||
+ (readMethod === ReadMethod.StencilTest && (!info.stencil || aspect === 'depth-only')) ||
+ (readMethod === ReadMethod.ColorBlending && !info.color) ||
+ // [1]: Test with depth/stencil sampling
+ (readMethod === ReadMethod.Sample && (info.depth || info.stencil)) ||
+ (aspect === 'depth-only' && !info.depth) ||
+ (aspect === 'stencil-only' && !info.stencil) ||
+ (aspect === 'all' && info.depth && info.stencil) ||
+ // Cannot copy from a packed depth format.
+ // [2]: Test copying out of the stencil aspect.
+ ((readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) &&
+ (format === 'depth24plus' || format === 'depth24plus-stencil8'))
+ );
+ })
+ .combine('mipLevelCount', kMipLevelCounts)
+ // 1D texture can only have a single mip level
+ .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1)
+ .combine('sampleCount', kSampleCounts)
+ .unless(
+ ({ readMethod, sampleCount }) =>
+ // We can only read from multisampled textures by sampling.
+ sampleCount > 1 &&
+ (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture)
+ )
+ // Multisampled textures may only have one mip
+ .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1)
+ .combine('uninitializeMethod', kUninitializeMethods)
+ .unless(({ dimension, readMethod, uninitializeMethod, format, sampleCount }) => {
+ const formatInfo = kTextureFormatInfo[format];
+ return (
+ dimension !== '2d' &&
+ (sampleCount > 1 ||
+ formatInfo.depth ||
+ formatInfo.stencil ||
+ readMethod === ReadMethod.DepthTest ||
+ readMethod === ReadMethod.StencilTest ||
+ readMethod === ReadMethod.ColorBlending ||
+ uninitializeMethod === UninitializeMethod.StoreOpClear)
+ );
+ })
+ .expandWithParams(function* ({ dimension }) {
+ switch (dimension) {
+ case '2d':
+ yield { layerCount: 1 as LayerCounts };
+ yield { layerCount: 7 as LayerCounts };
+ break;
+ case '1d':
+ case '3d':
+ yield { layerCount: 1 as LayerCounts };
+ break;
+ }
+ })
+ // Multisampled 3D / 2D array textures not supported.
+ .unless(({ sampleCount, layerCount }) => sampleCount > 1 && layerCount > 1)
+ .unless(({ format, sampleCount, uninitializeMethod, readMethod }) => {
+ const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod);
+ const info = kTextureFormatInfo[format];
+
+ return (
+ ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 && !info.renderable) ||
+ ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.storage) ||
+ (sampleCount > 1 && !info.multisample)
+ );
+ })
+ .combine('nonPowerOfTwo', [false, true])
+ .combine('canaryOnCreation', [false, true])
+ .filter(({ canaryOnCreation, format }) => {
+ // We can only initialize the texture if it's encodable or renderable.
+ const canInitialize = format in kTextureFormatInfo || kTextureFormatInfo[format].renderable;
+
+ // Filter out cases where we want canary values but can't initialize.
+ return !canaryOnCreation || canInitialize;
+ });
+
+type TextureZeroParams = ParamTypeOf<typeof kTestParams>;
+
+export type CheckContents = (
+ t: TextureZeroInitTest,
+ params: TextureZeroParams,
+ texture: GPUTexture,
+ state: InitializedState,
+ subresourceRange: SubresourceRange
+) => void;
+
+import { checkContentsByBufferCopy, checkContentsByTextureCopy } from './check_texture/by_copy.js';
+import {
+ checkContentsByDepthTest,
+ checkContentsByStencilTest,
+} from './check_texture/by_ds_test.js';
+import { checkContentsBySampling } from './check_texture/by_sampling.js';
+
+const checkContentsImpl: { [k in ReadMethod]: CheckContents } = {
+ Sample: checkContentsBySampling,
+ CopyToBuffer: checkContentsByBufferCopy,
+ CopyToTexture: checkContentsByTextureCopy,
+ DepthTest: checkContentsByDepthTest,
+ StencilTest: checkContentsByStencilTest,
+ ColorBlending: t => t.skip('Not implemented'),
+ Storage: t => t.skip('Not implemented'),
+};
+
+export const g = makeTestGroup(TextureZeroInitTest);
+
+g.test('uninitialized_texture_is_zero')
+ .params(kTestParams)
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[t.params.format].feature);
+ })
+ .fn(async t => {
+ const usage = getRequiredTextureUsage(
+ t.params.format,
+ t.params.sampleCount,
+ t.params.uninitializeMethod,
+ t.params.readMethod
+ );
+
+ const texture = t.device.createTexture({
+ size: [t.textureWidth, t.textureHeight, t.textureDepthOrArrayLayers],
+ format: t.params.format,
+ dimension: t.params.dimension,
+ usage,
+ mipLevelCount: t.params.mipLevelCount,
+ sampleCount: t.params.sampleCount,
+ });
+ t.trackForCleanup(texture);
+
+ if (t.params.canaryOnCreation) {
+ // Initialize some subresources with canary values
+ for (const subresourceRange of t.iterateInitializedSubresources()) {
+ t.initializeTexture(texture, InitializedState.Canary, subresourceRange);
+ }
+ }
+
+ switch (t.params.uninitializeMethod) {
+ case UninitializeMethod.Creation:
+ break;
+ case UninitializeMethod.StoreOpClear:
+ // Initialize the rest of the resources.
+ for (const subresourceRange of t.iterateUninitializedSubresources()) {
+ t.initializeTexture(texture, InitializedState.Canary, subresourceRange);
+ }
+ // Then use a store op to discard their contents.
+ for (const subresourceRange of t.iterateUninitializedSubresources()) {
+ t.discardTexture(texture, subresourceRange);
+ }
+ break;
+ default:
+ unreachable();
+ }
+
+ // Check that all uninitialized resources are zero.
+ for (const subresourceRange of t.iterateUninitializedSubresources()) {
+ checkContentsImpl[t.params.readMethod](
+ t,
+ t.params,
+ texture,
+ InitializedState.Zero,
+ subresourceRange
+ );
+ }
+
+ if (t.params.canaryOnCreation) {
+ // Check the all other resources are unchanged.
+ for (const subresourceRange of t.iterateInitializedSubresources()) {
+ checkContentsImpl[t.params.readMethod](
+ t,
+ t.params,
+ texture,
+ InitializedState.Canary,
+ subresourceRange
+ );
+ }
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts
new file mode 100644
index 0000000000..705f317f5b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts
@@ -0,0 +1,320 @@
+export const description = `
+Tests the behavior of anisotropic filtering.
+
+TODO:
+Note that anisotropic filtering is never guaranteed to occur, but we might be able to test some
+things. If there are no guarantees we can issue warnings instead of failures. Ideas:
+ - No *more* than the provided maxAnisotropy samples are used, by testing how many unique
+ sample values come out of the sample operation.
+ - Check anisotropy is done in the correct direction (by having a 2D gradient and checking we get
+ more of the color in the correct direction).
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { checkElementsEqual } from '../../../util/check_contents.js';
+
+const kRTSize = 16;
+const kBytesPerRow = 256;
+const xMiddle = kRTSize / 2; // we check the pixel value in the middle of the render target
+const kColorAttachmentFormat = 'rgba8unorm';
+const kTextureFormat = 'rgba8unorm';
+const colors = [
+ new Uint8Array([0xff, 0x00, 0x00, 0xff]), // miplevel = 0
+ new Uint8Array([0x00, 0xff, 0x00, 0xff]), // miplevel = 1
+ new Uint8Array([0x00, 0x00, 0xff, 0xff]), // miplevel = 2
+];
+const checkerColors = [
+ new Uint8Array([0xff, 0x00, 0x00, 0xff]),
+ new Uint8Array([0x00, 0xff, 0x00, 0xff]),
+];
+
+// renders texture a slanted plane placed in a specific way
+class SamplerAnisotropicFilteringSlantedPlaneTest extends GPUTest {
+ copyRenderTargetToBuffer(rt: GPUTexture): GPUBuffer {
+ const byteLength = kRTSize * kBytesPerRow;
+ const buffer = this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyTextureToBuffer(
+ { texture: rt, mipLevel: 0, origin: [0, 0, 0] },
+ { buffer, bytesPerRow: kBytesPerRow, rowsPerImage: kRTSize },
+ { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 }
+ );
+ this.queue.submit([commandEncoder.finish()]);
+
+ return buffer;
+ }
+
+ private pipeline: GPURenderPipeline | undefined;
+ async init(): Promise<void> {
+ await super.init();
+
+ this.pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Outputs {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) fragUV : vec2<f32>,
+ };
+
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32) -> Outputs {
+ var position : array<vec3<f32>, 6> = array<vec3<f32>, 6>(
+ vec3<f32>(-0.5, 0.5, -0.5),
+ vec3<f32>(0.5, 0.5, -0.5),
+ vec3<f32>(-0.5, 0.5, 0.5),
+ vec3<f32>(-0.5, 0.5, 0.5),
+ vec3<f32>(0.5, 0.5, -0.5),
+ vec3<f32>(0.5, 0.5, 0.5));
+ // uv is pre-scaled to mimic repeating tiled texture
+ var uv : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.0, 50.0),
+ vec2<f32>(0.0, 50.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 50.0));
+ // draw a slanted plane in a specific way
+ let matrix : mat4x4<f32> = mat4x4<f32>(
+ vec4<f32>(-1.7320507764816284, 1.8322050568049563e-16, -6.176817699518044e-17, -6.170640314703498e-17),
+ vec4<f32>(-2.1211504944260596e-16, -1.496108889579773, 0.5043753981590271, 0.5038710236549377),
+ vec4<f32>(0.0, -43.63650894165039, -43.232173919677734, -43.18894577026367),
+ vec4<f32>(0.0, 21.693578720092773, 21.789791107177734, 21.86800193786621));
+
+ var output : Outputs;
+ output.fragUV = uv[VertexIndex];
+ output.Position = matrix * vec4<f32>(position[VertexIndex], 1.0);
+ return output;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var sampler0 : sampler;
+ @group(0) @binding(1) var texture0 : texture_2d<f32>;
+
+ @fragment fn main(
+ @builtin(position) FragCoord : vec4<f32>,
+ @location(0) fragUV: vec2<f32>)
+ -> @location(0) vec4<f32> {
+ return textureSample(texture0, sampler0, fragUV);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ }
+
+ // return the render target texture object
+ drawSlantedPlane(textureView: GPUTextureView, sampler: GPUSampler): GPUTexture {
+ // make sure it's already initialized
+ assert(this.pipeline !== undefined);
+
+ const bindGroup = this.device.createBindGroup({
+ entries: [
+ { binding: 0, resource: sampler },
+ { binding: 1, resource: textureView },
+ ],
+ layout: this.pipeline.getBindGroupLayout(0),
+ });
+
+ const colorAttachment = this.device.createTexture({
+ format: kColorAttachmentFormat,
+ size: { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(this.pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(6);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ return colorAttachment;
+ }
+}
+
+export const g = makeTestGroup(SamplerAnisotropicFilteringSlantedPlaneTest);
+
+g.test('anisotropic_filter_checkerboard')
+ .desc(
+ `Anisotropic filter rendering tests that draws a slanted plane and samples from a texture
+ that only has a top level mipmap, the content of which is like a checkerboard.
+ We will check the rendering result using sampler with maxAnisotropy values to be
+ different from each other, as the sampling rate is different.
+ We will also check if those large maxAnisotropy values are clamped so that rendering is the
+ same as the supported upper limit say 16.
+ A similar webgl demo is at https://jsfiddle.net/yqnbez24`
+ )
+ .fn(async t => {
+ // init texture with only a top level mipmap
+ const textureSize = 32;
+ const texture = t.device.createTexture({
+ mipLevelCount: 1,
+ size: { width: textureSize, height: textureSize, depthOrArrayLayers: 1 },
+ format: kTextureFormat,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ const textureEncoder = t.device.createCommandEncoder();
+
+ const bufferSize = kBytesPerRow * textureSize; // RGBA8 for each pixel (256 > 16 * 4)
+
+ // init checkerboard texture data
+ const data: Uint8Array = new Uint8Array(bufferSize);
+ for (let r = 0; r < textureSize; r++) {
+ const o = r * kBytesPerRow;
+ for (let c = o, end = o + textureSize * 4; c < end; c += 4) {
+ const cid = (r + (c - o) / 4) % 2;
+ const color = checkerColors[cid];
+ data[c] = color[0];
+ data[c + 1] = color[1];
+ data[c + 2] = color[2];
+ data[c + 3] = color[3];
+ }
+ }
+ const buffer = t.makeBufferWithContents(
+ data,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+ const bytesPerRow = kBytesPerRow;
+ const rowsPerImage = textureSize;
+
+ textureEncoder.copyBufferToTexture(
+ {
+ buffer,
+ bytesPerRow,
+ rowsPerImage,
+ },
+ {
+ texture,
+ mipLevel: 0,
+ origin: [0, 0, 0],
+ },
+ [textureSize, textureSize, 1]
+ );
+
+ t.device.queue.submit([textureEncoder.finish()]);
+
+ const textureView = texture.createView();
+ const byteLength = kRTSize * kBytesPerRow;
+ const results = [];
+
+ for (const maxAnisotropy of [1, 16, 1024]) {
+ const sampler = t.device.createSampler({
+ magFilter: 'linear',
+ minFilter: 'linear',
+ mipmapFilter: 'linear',
+ maxAnisotropy,
+ });
+ const result = await t.readGPUBufferRangeTyped(
+ t.copyRenderTargetToBuffer(t.drawSlantedPlane(textureView, sampler)),
+ { type: Uint8Array, typedLength: byteLength }
+ );
+ results.push(result);
+ }
+
+ const check0 = checkElementsEqual(results[0].data, results[1].data);
+ if (check0 === undefined) {
+ t.warn('Render results with sampler.maxAnisotropy being 1 and 16 should be different.');
+ }
+ const check1 = checkElementsEqual(results[1].data, results[2].data);
+ if (check1 !== undefined) {
+ t.expect(
+ false,
+ 'Render results with sampler.maxAnisotropy being 16 and 1024 should be the same.'
+ );
+ }
+
+ for (const result of results) {
+ result.cleanup();
+ }
+ });
+
+g.test('anisotropic_filter_mipmap_color')
+ .desc(
+ `Anisotropic filter rendering tests that draws a slanted plane and samples from a texture
+ containing mipmaps of different colors. Given the same fragment with dFdx and dFdy for uv being different,
+ sampler with bigger maxAnisotropy value tends to bigger mip levels to provide better details.
+ We can then look at the color of the fragment to know which mip level is being sampled from and to see
+ if it fits expectations.
+ A similar webgl demo is at https://jsfiddle.net/t8k7c95o/5/`
+ )
+ .paramsSimple([
+ {
+ maxAnisotropy: 1,
+ _results: [
+ { coord: { x: xMiddle, y: 2 }, expected: colors[2] },
+ { coord: { x: xMiddle, y: 6 }, expected: [colors[0], colors[1]] },
+ ],
+ _generateWarningOnly: false,
+ },
+ {
+ maxAnisotropy: 4,
+ _results: [
+ { coord: { x: xMiddle, y: 2 }, expected: [colors[0], colors[1]] },
+ { coord: { x: xMiddle, y: 6 }, expected: colors[0] },
+ ],
+ _generateWarningOnly: true,
+ },
+ ])
+ .fn(async t => {
+ const texture = t.createTexture2DWithMipmaps(colors);
+
+ const textureView = texture.createView();
+
+ const sampler = t.device.createSampler({
+ magFilter: 'linear',
+ minFilter: 'linear',
+ mipmapFilter: 'linear',
+ maxAnisotropy: t.params.maxAnisotropy,
+ });
+
+ const colorAttachment = t.drawSlantedPlane(textureView, sampler);
+
+ for (const entry of t.params._results) {
+ if (entry.expected instanceof Uint8Array) {
+ // equal exactly one color
+ t.expectSinglePixelIn2DTexture(colorAttachment, kColorAttachmentFormat, entry.coord, {
+ exp: entry.expected,
+ generateWarningOnly: t.params._generateWarningOnly,
+ });
+ } else {
+ // a lerp between two colors
+ t.expectSinglePixelBetweenTwoValuesIn2DTexture(
+ colorAttachment,
+ kColorAttachmentFormat,
+ entry.coord,
+ {
+ exp: entry.expected as [Uint8Array, Uint8Array],
+ generateWarningOnly: t.params._generateWarningOnly,
+ }
+ );
+ }
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/filter_mode.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/filter_mode.spec.ts
new file mode 100644
index 0000000000..cf1d7682e1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/filter_mode.spec.ts
@@ -0,0 +1,14 @@
+export const description = `
+Tests the behavior of different filtering modes in minFilter/magFilter/mipmapFilter.
+
+TODO:
+- Test exact sampling results with small tolerance. Tests should differentiate between different
+ values for all three filter modes to make sure none are missed or incorrect in implementations.
+- (Likely unnecessary with the above.) Test exactly the expected number of samples are used.
+ Test this by setting up a rendering and asserting how many different shades result.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/lod_clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/lod_clamp.spec.ts
new file mode 100644
index 0000000000..8ef35422dc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/lod_clamp.spec.ts
@@ -0,0 +1,12 @@
+export const description = `
+Tests the behavior of LOD clamping (lodMinClamp, lodMaxClamp).
+
+TODO:
+- Write a test that can test the exact clamping behavior
+- Test a bunch of values, including very large/small ones.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts
new file mode 100644
index 0000000000..bbd02892eb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/shader_module/compilation_info.spec.ts
@@ -0,0 +1,197 @@
+export const description = `
+ShaderModule CompilationInfo tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const kValidShaderSources = [
+ {
+ valid: true,
+ name: 'ascii',
+ _code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ },
+ {
+ valid: true,
+ name: 'unicode',
+ _code: `
+ // 頂点シェーダー 👩‍💻
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ },
+];
+
+const kInvalidShaderSources = [
+ {
+ valid: false,
+ name: 'ascii',
+ _errorLine: 4,
+ _code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ // Expected Error: unknown function 'unknown'
+ return unknown(0.0, 0.0, 0.0, 1.0);
+ }`,
+ },
+ {
+ valid: false,
+ name: 'unicode',
+ _errorLine: 5,
+ _code: `
+ // 頂点シェーダー 👩‍💻
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ // Expected Error: unknown function 'unknown'
+ return unknown(0.0, 0.0, 0.0, 1.0);
+ }`,
+ },
+ {
+ valid: false,
+ name: 'carriage-return',
+ _errorLine: 5,
+ _code:
+ `
+ @vertex fn main() -> @builtin(position) vec4<f32> {` +
+ '\r\n' +
+ `
+ // Expected Error: unknown function 'unknown'
+ return unknown(0.0, 0.0, 0.0, 1.0);
+ }`,
+ },
+];
+
+const kAllShaderSources = [...kValidShaderSources, ...kInvalidShaderSources];
+
+g.test('compilationInfo_returns')
+ .desc(
+ `
+ Test that compilationInfo() can be called on any ShaderModule.
+ - Test for both valid and invalid shader modules.
+ - Test for shader modules containing only ASCII and those containing unicode characters.
+ - Test that the compilation info for valid shader modules contains no errors.
+ - Test that the compilation info for invalid shader modules contains at least one error.`
+ )
+ .paramsSimple(kAllShaderSources)
+ .fn(async t => {
+ const { _code, valid } = t.params;
+
+ const shaderModule = t.expectGPUError(
+ 'validation',
+ () => t.device.createShaderModule({ code: _code }),
+ !valid
+ );
+
+ const info = await shaderModule.compilationInfo();
+
+ t.expect(
+ info instanceof GPUCompilationInfo,
+ 'Expected a GPUCompilationInfo object to be returned'
+ );
+
+ // Expect that we get zero error messages from a valid shader.
+ // Message types other than errors are OK.
+ let errorCount = 0;
+ for (const message of info.messages) {
+ if (message.type === 'error') {
+ errorCount++;
+ }
+ }
+ if (valid) {
+ t.expect(errorCount === 0, "Expected zero GPUCompilationMessages of type 'error'");
+ } else {
+ t.expect(errorCount > 0, "Expected at least one GPUCompilationMessages of type 'error'");
+ }
+ });
+
+g.test('line_number_and_position')
+ .desc(
+ `
+ Test that line numbers reported by compilationInfo either point at an appropriate line and
+ position or at 0:0, indicating an unknown position.
+ - Test for invalid shader modules containing containing at least one error.
+ - Test for shader modules containing only ASCII and those containing unicode characters.`
+ )
+ .paramsSimple(kInvalidShaderSources)
+ .fn(async t => {
+ const { _code, _errorLine } = t.params;
+
+ const shaderModule = t.expectGPUError('validation', () =>
+ t.device.createShaderModule({ code: _code })
+ );
+
+ const info = await shaderModule.compilationInfo();
+
+ let foundAppropriateError = false;
+ for (const message of info.messages) {
+ if (message.type === 'error') {
+ // Some backends may not be able to indicate a precise location for the error. In those
+ // cases a line and position of 0 should be reported.
+ // If a line is reported, it should point at the correct line (1-based).
+ t.expect(
+ (message.lineNum === 0) === (message.linePos === 0),
+ "GPUCompilationMessages that don't report a line number should not report a line position."
+ );
+
+ if (message.lineNum === 0 || message.lineNum === _errorLine) {
+ foundAppropriateError = true;
+
+ // Various backends may choose to report the error at different positions within the line,
+ // so it's difficult to meaningfully validate them.
+ break;
+ }
+ }
+ }
+ t.expect(
+ foundAppropriateError,
+ 'Expected to find an error which corresponded with the erroneous line'
+ );
+ });
+
+g.test('offset_and_length')
+ .desc(
+ `Test that message offsets and lengths are valid and align with any reported lineNum and linePos.
+ - Test for valid and invalid shader modules.
+ - Test for shader modules containing only ASCII and those containing unicode characters.`
+ )
+ .paramsSimple(kAllShaderSources)
+ .fn(async t => {
+ const { _code, valid } = t.params;
+
+ const shaderModule = t.expectGPUError(
+ 'validation',
+ () => t.device.createShaderModule({ code: _code }),
+ !valid
+ );
+
+ const info = await shaderModule.compilationInfo();
+
+ for (const message of info.messages) {
+ // Any offsets and lengths should reference valid spans of the shader code.
+ t.expect(message.offset <= _code.length, 'Message offset should be within the shader source');
+ t.expect(
+ message.offset + message.length <= _code.length,
+ 'Message offset and length should be within the shader source'
+ );
+
+ // If a valid line number and position are given, the offset should point the the same
+ // location in the shader source.
+ if (message.lineNum !== 0 && message.linePos !== 0) {
+ let lineOffset = 0;
+ for (let i = 0; i < message.lineNum - 1; ++i) {
+ lineOffset = _code.indexOf('\n', lineOffset);
+ assert(lineOffset !== -1);
+ lineOffset += 1;
+ }
+
+ t.expect(
+ message.offset === lineOffset + message.linePos - 1,
+ 'lineNum and linePos should point to the same location as offset'
+ );
+ }
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts
new file mode 100644
index 0000000000..30c8ddd962
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/format_reinterpretation.spec.ts
@@ -0,0 +1,362 @@
+export const description = `
+Test texture views can reinterpret the format of the original texture.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ EncodableTextureFormat,
+ kRenderableColorTextureFormats,
+ kRegularTextureFormats,
+ viewCompatible,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { TexelView } from '../../../util/texture/texel_view.js';
+import { textureContentIsOKByT2B } from '../../../util/texture/texture_ok.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const kColors = [
+ { R: 1.0, G: 0.0, B: 0.0, A: 0.8 },
+ { R: 0.0, G: 1.0, B: 0.0, A: 0.7 },
+ { R: 0.0, G: 0.0, B: 0.0, A: 0.6 },
+ { R: 0.0, G: 0.0, B: 0.0, A: 0.5 },
+ { R: 1.0, G: 1.0, B: 1.0, A: 0.4 },
+ { R: 0.7, G: 0.0, B: 0.0, A: 0.3 },
+ { R: 0.0, G: 0.8, B: 0.0, A: 0.2 },
+ { R: 0.0, G: 0.0, B: 0.9, A: 0.1 },
+ { R: 0.1, G: 0.2, B: 0.0, A: 0.3 },
+ { R: 0.4, G: 0.3, B: 0.6, A: 0.8 },
+];
+
+const kTextureSize = 16;
+
+function makeInputTexelView(format: EncodableTextureFormat) {
+ return TexelView.fromTexelsAsColors(
+ format,
+ coords => {
+ const pixelPos = coords.y * kTextureSize + coords.x;
+ return kColors[pixelPos % kColors.length];
+ },
+ { clampToFormatRange: true }
+ );
+}
+
+function makeBlitPipeline(
+ device: GPUDevice,
+ format: GPUTextureFormat,
+ multisample: { sample: number; render: number }
+) {
+ return device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: device.createShaderModule({
+ code: `
+ @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>( 1.0, 1.0));
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module:
+ multisample.sample > 1
+ ? device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var src: texture_multisampled_2d<f32>;
+ @fragment fn main(@builtin(position) coord: vec4<f32>) -> @location(0) vec4<f32> {
+ var result : vec4<f32>;
+ for (var i = 0; i < ${multisample.sample}; i = i + 1) {
+ result = result + textureLoad(src, vec2<i32>(coord.xy), i);
+ }
+ return result * ${1 / multisample.sample};
+ }`,
+ })
+ : device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var src: texture_2d<f32>;
+ @fragment fn main(@builtin(position) coord: vec4<f32>) -> @location(0) vec4<f32> {
+ return textureLoad(src, vec2<i32>(coord.xy), 0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ multisample: {
+ count: multisample.render,
+ },
+ });
+}
+
+g.test('texture_binding')
+ .desc(`Test that a regular texture allocated as 'format' is correctly sampled as 'viewFormat'.`)
+ .params(u =>
+ u //
+ .combine('format', kRegularTextureFormats)
+ .combine('viewFormat', kRegularTextureFormats)
+ .filter(
+ ({ format, viewFormat }) => format !== viewFormat && viewCompatible(format, viewFormat)
+ )
+ )
+ .fn(async t => {
+ const { format, viewFormat } = t.params;
+
+ // Make an input texel view.
+ const inputTexelView = makeInputTexelView(format);
+
+ // Create the initial texture with the contents if the input texel view.
+ const texture = t.makeTextureWithContents(inputTexelView, {
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ viewFormats: [viewFormat],
+ });
+
+ // Reinterpret the texture as the view format.
+ // Make a texel view of the format that also reinterprets the data.
+ const reinterpretedView = texture.createView({ format: viewFormat });
+ const reinterpretedTexelView = TexelView.fromTexelsAsBytes(viewFormat, inputTexelView.bytes);
+
+ // Create a pipeline to write data out to rgba8unorm.
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var src: texture_2d<f32>;
+ @group(0) @binding(1) var dst: texture_storage_2d<rgba8unorm, write>;
+ @compute @workgroup_size(1, 1) fn main(
+ @builtin(global_invocation_id) global_id: vec3<u32>,
+ ) {
+ var coord = vec2<i32>(global_id.xy);
+ textureStore(dst, coord, textureLoad(src, coord, 0));
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ // Create an rgba8unorm output texture.
+ const outputTexture = t.trackForCleanup(
+ t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_SRC,
+ })
+ );
+
+ // Execute a compute pass to load data from the reinterpreted view and
+ // write out to the rgba8unorm texture.
+ const commandEncoder = t.device.createCommandEncoder();
+ const pass = commandEncoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(
+ 0,
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: reinterpretedView,
+ },
+ {
+ binding: 1,
+ resource: outputTexture.createView(),
+ },
+ ],
+ })
+ );
+ pass.dispatchWorkgroups(kTextureSize, kTextureSize);
+ pass.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: outputTexture },
+ [kTextureSize, kTextureSize],
+ {
+ expTexelView: TexelView.fromTexelsAsColors('rgba8unorm', reinterpretedTexelView.color, {
+ clampToFormatRange: true,
+ }),
+ },
+ { maxDiffULPsForNormFormat: 1 }
+ );
+ t.expectOK(result);
+ });
+
+g.test('render_and_resolve_attachment')
+ .desc(
+ `Test that a color render attachment allocated as 'format' is correctly rendered to as 'viewFormat',
+and resolved to an attachment allocated as 'format' viewed as 'viewFormat'.
+
+Other combinations aren't possible because the render and resolve targets must both match
+in view format and match in base format.`
+ )
+ .params(u =>
+ u //
+ .combine('format', kRenderableColorTextureFormats)
+ .combine('viewFormat', kRenderableColorTextureFormats)
+ .filter(
+ ({ format, viewFormat }) => format !== viewFormat && viewCompatible(format, viewFormat)
+ )
+ .combine('sampleCount', [1, 4])
+ )
+ .fn(async t => {
+ const { format, viewFormat, sampleCount } = t.params;
+
+ // Make an input texel view.
+ const inputTexelView = makeInputTexelView(format);
+
+ // Create the renderTexture as |format|.
+ const renderTexture = t.trackForCleanup(
+ t.device.createTexture({
+ format,
+ size: [kTextureSize, kTextureSize],
+ usage:
+ GPUTextureUsage.RENDER_ATTACHMENT |
+ (sampleCount > 1 ? GPUTextureUsage.TEXTURE_BINDING : GPUTextureUsage.COPY_SRC),
+ viewFormats: [viewFormat],
+ sampleCount,
+ })
+ );
+
+ const resolveTexture =
+ sampleCount === 1
+ ? undefined
+ : t.trackForCleanup(
+ t.device.createTexture({
+ format,
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ viewFormats: [viewFormat],
+ })
+ );
+
+ // Create the sample source with the contents of the input texel view.
+ // We will sample this texture into |renderTexture|. It uses the same format to keep the same
+ // number of bits of precision.
+ const sampleSource = t.makeTextureWithContents(inputTexelView, {
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ // Reinterpret the renderTexture as |viewFormat|.
+ const reinterpretedRenderView = renderTexture.createView({ format: viewFormat });
+ const reinterpretedResolveView =
+ resolveTexture && resolveTexture.createView({ format: viewFormat });
+
+ // Create a pipeline to blit a src texture to the render attachment.
+ const pipeline = makeBlitPipeline(t.device, viewFormat, {
+ sample: 1,
+ render: sampleCount,
+ });
+
+ // Execute a render pass to sample |sampleSource| into |texture| viewed as |viewFormat|.
+ const commandEncoder = t.device.createCommandEncoder();
+ const pass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: reinterpretedRenderView,
+ resolveTarget: reinterpretedResolveView,
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(
+ 0,
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: sampleSource.createView(),
+ },
+ ],
+ })
+ );
+ pass.draw(6);
+ pass.end();
+
+ // If the render target is multisampled, we'll manually resolve it to check
+ // the contents.
+ const singleSampleRenderTexture = resolveTexture
+ ? t.trackForCleanup(
+ t.device.createTexture({
+ format,
+ size: [kTextureSize, kTextureSize],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ )
+ : renderTexture;
+
+ if (resolveTexture) {
+ // Create a pipeline to blit the multisampled render texture to a non-multisample texture.
+ // We are basically performing a manual resolve step to the same format as the original
+ // render texture to check its contents.
+ const pipeline = makeBlitPipeline(t.device, format, { sample: sampleCount, render: 1 });
+ const pass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: singleSampleRenderTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(
+ 0,
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: renderTexture.createView(),
+ },
+ ],
+ })
+ );
+ pass.draw(6);
+ pass.end();
+ }
+
+ // Submit the commands.
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ // Check the rendered contents.
+ const renderViewTexels = TexelView.fromTexelsAsColors(viewFormat, inputTexelView.color, {
+ clampToFormatRange: true,
+ });
+ t.expectOK(
+ await textureContentIsOKByT2B(
+ t,
+ { texture: singleSampleRenderTexture },
+ [kTextureSize, kTextureSize],
+ { expTexelView: renderViewTexels },
+ { maxDiffULPsForNormFormat: 2 }
+ )
+ );
+
+ // Check the resolved contents.
+ if (resolveTexture) {
+ const resolveView = TexelView.fromTexelsAsColors(viewFormat, renderViewTexels.color, {
+ clampToFormatRange: true,
+ });
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture: resolveTexture },
+ [kTextureSize, kTextureSize],
+ { expTexelView: resolveView },
+ { maxDiffULPsForNormFormat: 2 }
+ );
+ t.expectOK(result);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/read.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/read.spec.ts
new file mode 100644
index 0000000000..ce2e5055a8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/read.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Test the result of reading textures through texture views with various options.
+
+All x= every possible view read method: {
+ - {unfiltered, filtered (if valid), comparison (if valid)} sampling
+ - storage read {vertex, fragment, compute}
+ - no-op render pass that loads and then stores
+ - depth comparison
+ - stencil comparison
+}
+
+Format reinterpretation is not tested here. It is in format_reinterpretation.spec.ts.
+
+TODO: Write helper for this if not already available (see resource_init, buffer_sync_test for related code).
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('format')
+ .desc(
+ `Views of every allowed format.
+
+- x= every texture format
+- x= sampleCount {1, 4} if valid
+- x= every possible view read method (see above)
+`
+ )
+ .unimplemented();
+
+g.test('dimension')
+ .desc(
+ `Views of every allowed dimension.
+
+- x= a representative subset of formats
+- x= {every texture dimension} x {every valid view dimension}
+ (per gpuweb#79 no dimension-count reinterpretations, like 2d-array <-> 3d, are possible)
+- x= sampleCount {1, 4} if valid
+- x= every possible view read method (see above)
+`
+ )
+ .unimplemented();
+
+g.test('aspect')
+ .desc(
+ `Views of every allowed aspect of depth/stencil textures.
+
+- x= every depth/stencil format
+- x= {"all", "stencil-only", "depth-only"} where valid for the format
+- x= sampleCount {1, 4} if valid
+- x= every possible view read method (see above)
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts
new file mode 100644
index 0000000000..0340121334
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/texture_view/write.spec.ts
@@ -0,0 +1,54 @@
+export const description = `
+Test the result of writing textures through texture views with various options.
+
+All x= every possible view write method: {
+ - storage write {fragment, compute}
+ - render pass store
+ - render pass resolve
+}
+
+Format reinterpretation is not tested here. It is in format_reinterpretation.spec.ts.
+
+TODO: Write helper for this if not already available (see resource_init, buffer_sync_test for related code).
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('format')
+ .desc(
+ `Views of every allowed format.
+
+- x= every texture format
+- x= sampleCount {1, 4} if valid
+- x= every possible view write method (see above)
+`
+ )
+ .unimplemented();
+
+g.test('dimension')
+ .desc(
+ `Views of every allowed dimension.
+
+- x= a representative subset of formats
+- x= {every texture dimension} x {every valid view dimension}
+ (per gpuweb#79 no dimension-count reinterpretations, like 2d-array <-> 3d, are possible)
+- x= sampleCount {1, 4} if valid
+- x= every possible view write method (see above)
+`
+ )
+ .unimplemented();
+
+g.test('aspect')
+ .desc(
+ `Views of every allowed aspect of depth/stencil textures.
+
+- x= every depth/stencil format
+- x= {"all", "stencil-only", "depth-only"} where valid for the format
+- x= sampleCount {1, 4} if valid
+- x= every possible view write method (see above)
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/threading/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/threading/README.txt
new file mode 100644
index 0000000000..caccf6f69d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/threading/README.txt
@@ -0,0 +1,11 @@
+Tests for behavior with multiple threads (main thread + workers).
+
+TODO: plan and implement
+- 'postMessage'
+ Try postMessage'ing an object of every type (to same or different thread)
+ - {main -> main, main -> worker, worker -> main, worker1 -> worker1, worker1 -> worker2}
+ - through {global postMessage, MessageChannel}
+ - {in, not in} transferrable object list, when valid
+- 'concurrency'
+ Short tight loop doing many of an action from two threads at the same time
+ - e.g. {create {buffer, texture, shader, pipeline}}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/uncapturederror.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/uncapturederror.spec.ts
new file mode 100644
index 0000000000..c957f55fb3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/uncapturederror.spec.ts
@@ -0,0 +1,34 @@
+export const description = `
+Tests for GPUDevice.onuncapturederror.
+`;
+
+import { Fixture } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+export const g = makeTestGroup(Fixture);
+
+g.test('constructor')
+ .desc(
+ `GPUUncapturedErrorEvent constructor options (also tests constructing GPUOutOfMemoryError/GPUValidationError)`
+ )
+ .unimplemented();
+
+g.test('iff_uncaptured')
+ .desc(
+ `{validation, out-of-memory} error should fire uncapturederror iff not captured by a scope.`
+ )
+ .unimplemented();
+
+g.test('only_original_device_is_event_target')
+ .desc(
+ `Original GPUDevice objects are EventTargets and have onuncapturederror, but
+deserialized GPUDevices do not.`
+ )
+ .unimplemented();
+
+g.test('uncapturederror_from_non_originating_thread')
+ .desc(
+ `Uncaptured errors on any thread should always propagate to the original GPUDevice object
+(since deserialized ones don't have EventTarget/onuncapturederror).`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/correctness.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/correctness.spec.ts
new file mode 100644
index 0000000000..678e0f648f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/correctness.spec.ts
@@ -0,0 +1,1095 @@
+export const description = `
+TODO: Test more corner case values for Float16 / Float32 (INF, NaN, +-0, ...) and reduce the
+float tolerance.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, memcpy, unreachable } from '../../../../common/util/util.js';
+import {
+ kMaxVertexAttributes,
+ kMaxVertexBufferArrayStride,
+ kMaxVertexBuffers,
+ kPerStageBindingLimits,
+ kVertexFormatInfo,
+ kVertexFormats,
+} from '../../../capability_info.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { float32ToFloat16Bits, normalizedIntegerAsFloat } from '../../../util/conversion.js';
+import { align, clamp } from '../../../util/math.js';
+
+// These types mirror the structure of GPUVertexBufferLayout but allow defining the extra
+// dictionary members at the GPUVertexBufferLayout and GPUVertexAttribute level. The are used
+// like so:
+//
+// VertexState<{arrayStride: number}, {format: VertexFormat}>
+// VertexBuffer<{arrayStride: number}, {format: VertexFormat}>
+// VertexAttrib<{format: VertexFormat}>
+type VertexAttrib<A> = A & { shaderLocation: number };
+type VertexBuffer<V, A> = V & {
+ slot: number;
+ attributes: VertexAttrib<A>[];
+};
+type VertexState<V, A> = VertexBuffer<V, A>[];
+
+type VertexLayoutState<V, A> = VertexState<
+ { stepMode: GPUVertexStepMode; arrayStride: number } & V,
+ { format: GPUVertexFormat; offset: number } & A
+>;
+
+function mapBufferAttribs<V, A1, A2>(
+ buffer: VertexBuffer<V, A1>,
+ f: (v: V, a: VertexAttrib<A1>) => A2
+): VertexBuffer<V, A2> {
+ const newAttributes: VertexAttrib<A2>[] = [];
+ for (const a of buffer.attributes) {
+ newAttributes.push({
+ shaderLocation: a.shaderLocation,
+ ...f(buffer, a),
+ });
+ }
+
+ return { ...buffer, attributes: newAttributes };
+}
+
+function mapStateAttribs<V, A1, A2>(
+ buffers: VertexState<V, A1>,
+ f: (v: V, a: VertexAttrib<A1>) => A2
+): VertexState<V, A2> {
+ return buffers.map(b => mapBufferAttribs(b, f));
+}
+
+type TestData = {
+ shaderBaseType: string;
+ floatTolerance?: number;
+ // The number of vertex components in the vertexData (expectedData might contain more because
+ // it is padded to 4 components).
+ testComponentCount: number;
+ // The data that will be in the uniform buffer and used to check the vertex inputs.
+ expectedData: ArrayBuffer;
+ // The data that will be in the vertex buffer.
+ vertexData: ArrayBuffer;
+};
+
+class VertexStateTest extends GPUTest {
+ // Generate for VS + FS (entrypoints vsMain / fsMain) that for each attribute will check that its
+ // value corresponds to what's expected (as provided by a uniform buffer per attribute) and then
+ // renders each vertex at position (vertexIndex, instanceindex) with either 1 (success) or
+ // a negative number corresponding to the check number (in case you need to debug a failure).
+ makeTestWGSL(
+ buffers: VertexState<
+ { stepMode: GPUVertexStepMode },
+ {
+ format: GPUVertexFormat;
+ shaderBaseType: string;
+ shaderComponentCount?: number;
+ floatTolerance?: number;
+ }
+ >,
+ vertexCount: number,
+ instanceCount: number
+ ): string {
+ // In the base WebGPU spec maxVertexAttributes is larger than maxUniformBufferPerStage. We'll
+ // use a combination of uniform and storage buffers to cover all possible attributes. This
+ // happens to work because maxUniformBuffer + maxStorageBuffer = 12 + 8 = 20 which is larger
+ // than maxVertexAttributes = 16.
+ // However this might not work in the future for implementations that allow even more vertex
+ // attributes so there will need to be larger changes when that happens.
+ const maxUniformBuffers = kPerStageBindingLimits['uniformBuf'].max;
+ assert(maxUniformBuffers + kPerStageBindingLimits['storageBuf'].max >= kMaxVertexAttributes);
+
+ let vsInputs = '';
+ let vsChecks = '';
+ let vsBindings = '';
+
+ for (const b of buffers) {
+ for (const a of b.attributes) {
+ const format = kVertexFormatInfo[a.format];
+ const shaderComponentCount = a.shaderComponentCount ?? format.componentCount;
+ const i = a.shaderLocation;
+
+ // shaderType is either a scalar type like f32 or a vecN<scalarType>
+ let shaderType = a.shaderBaseType;
+ if (shaderComponentCount !== 1) {
+ shaderType = `vec${shaderComponentCount}<${shaderType}>`;
+ }
+
+ let maxCount = `${vertexCount}`;
+ let indexBuiltin = `input.vertexIndex`;
+ if (b.stepMode === 'instance') {
+ maxCount = `${instanceCount}`;
+ indexBuiltin = `input.instanceIndex`;
+ }
+
+ // Start using storage buffers when we run out of uniform buffers.
+ let storageType = 'uniform';
+ if (i >= maxUniformBuffers) {
+ storageType = 'storage, read';
+ }
+
+ vsInputs += ` @location(${i}) attrib${i} : ${shaderType},\n`;
+ vsBindings += `struct S${i} { data : array<vec4<${a.shaderBaseType}>, ${maxCount}> };\n`;
+ vsBindings += `@group(0) @binding(${i}) var<${storageType}> providedData${i} : S${i};\n`;
+
+ // Generate the all the checks for the attributes.
+ for (let component = 0; component < shaderComponentCount; component++) {
+ // Components are filled with (0, 0, 0, 1) if they aren't provided data from the pipeline.
+ if (component >= format.componentCount) {
+ const expected = component === 3 ? '1' : '0';
+ vsChecks += ` check(input.attrib${i}[${component}] == ${a.shaderBaseType}(${expected}));\n`;
+ continue;
+ }
+
+ // Check each component individually, with special handling of tolerance for floats.
+ const attribComponent =
+ shaderComponentCount === 1 ? `input.attrib${i}` : `input.attrib${i}[${component}]`;
+ const providedData = `providedData${i}.data[${indexBuiltin}][${component}]`;
+ if (format.type === 'uint' || format.type === 'sint') {
+ vsChecks += ` check(${attribComponent} == ${providedData});\n`;
+ } else {
+ vsChecks += ` check(floatsSimilar(${attribComponent}, ${providedData}, f32(${
+ a.floatTolerance ?? 0
+ })));\n`;
+ }
+ }
+ }
+ }
+
+ return `
+struct Inputs {
+${vsInputs}
+ @builtin(vertex_index) vertexIndex: u32,
+ @builtin(instance_index) instanceIndex: u32,
+};
+
+${vsBindings}
+
+var<private> vsResult : i32 = 1;
+var<private> checkIndex : i32 = 0;
+fn check(success : bool) {
+ if (!success) {
+ vsResult = -checkIndex;
+ }
+ checkIndex = checkIndex + 1;
+}
+
+fn floatsSimilar(a : f32, b : f32, tolerance : f32) -> bool {
+ // TODO do we check for + and - 0?
+ return abs(a - b) < tolerance;
+}
+
+fn doTest(input : Inputs) {
+${vsChecks}
+}
+
+struct VSOutputs {
+ @location(0) @interpolate(flat) result : i32,
+ @builtin(position) position : vec4<f32>,
+};
+
+@vertex fn vsMain(input : Inputs) -> VSOutputs {
+ doTest(input);
+
+ // Place that point at pixel (vertexIndex, instanceIndex) in a framebuffer of size
+ // (vertexCount , instanceCount).
+ var output : VSOutputs;
+ output.position = vec4<f32>(
+ ((f32(input.vertexIndex) + 0.5) / ${vertexCount}.0 * 2.0) - 1.0,
+ ((f32(input.instanceIndex) + 0.5) / ${instanceCount}.0 * 2.0) - 1.0,
+ 0.0, 1.0
+ );
+ output.result = vsResult;
+ return output;
+}
+
+@fragment fn fsMain(@location(0) @interpolate(flat) result : i32)
+ -> @location(0) i32 {
+ return result;
+}
+ `;
+ }
+
+ makeTestPipeline(
+ buffers: VertexState<
+ { stepMode: GPUVertexStepMode; arrayStride: number },
+ {
+ offset: number;
+ format: GPUVertexFormat;
+ shaderBaseType: string;
+ shaderComponentCount?: number;
+ floatTolerance?: number;
+ }
+ >,
+ vertexCount: number,
+ instanceCount: number
+ ): GPURenderPipeline {
+ const module = this.device.createShaderModule({
+ code: this.makeTestWGSL(buffers, vertexCount, instanceCount),
+ });
+
+ const bufferLayouts: GPUVertexBufferLayout[] = [];
+ for (const b of buffers) {
+ bufferLayouts[b.slot] = b;
+ }
+
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vsMain',
+ buffers: bufferLayouts,
+ },
+ primitive: {
+ topology: 'point-list',
+ },
+ fragment: {
+ module,
+ entryPoint: 'fsMain',
+ targets: [
+ {
+ format: 'r32sint',
+ },
+ ],
+ },
+ });
+ }
+
+ // Runs the render pass drawing points in a vertexCount*instanceCount rectangle, then check each
+ // of produced a value of 1 which means that the tests in the shader passed.
+ submitRenderPass(
+ pipeline: GPURenderPipeline,
+ buffers: VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}>,
+ expectedData: GPUBindGroup,
+ vertexCount: number,
+ instanceCount: number
+ ) {
+ const testTexture = this.device.createTexture({
+ format: 'r32sint',
+ size: [vertexCount, instanceCount],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: testTexture.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, expectedData);
+ for (const buffer of buffers) {
+ pass.setVertexBuffer(buffer.slot, buffer.buffer, buffer.vbOffset ?? 0);
+ }
+ pass.draw(vertexCount, instanceCount);
+ pass.end();
+
+ this.device.queue.submit([encoder.finish()]);
+
+ this.expectSingleColor(testTexture, 'r32sint', {
+ size: [vertexCount, instanceCount, 1],
+ exp: { R: 1 },
+ });
+ }
+
+ // Generate TestData for the format with interesting test values.
+ // MAINTENANCE_TODO cache the result on the fixture?
+ // Note that the test data always starts with an interesting value, so that using the first
+ // test value in a test is still meaningful.
+ generateTestData(format: GPUVertexFormat): TestData {
+ const formatInfo = kVertexFormatInfo[format];
+ const bitSize = formatInfo.bytesPerComponent * 8;
+
+ switch (formatInfo.type) {
+ case 'float': {
+ const data = [42.42, 0.0, 1.0, -1.0, 1000, -18.7, 25.17];
+ const expectedData = new Float32Array(data).buffer;
+ const vertexData =
+ bitSize === 32
+ ? expectedData
+ : bitSize === 16
+ ? new Uint16Array(data.map(float32ToFloat16Bits)).buffer
+ : unreachable();
+
+ return {
+ shaderBaseType: 'f32',
+ testComponentCount: data.length,
+ expectedData,
+ vertexData,
+ floatTolerance: 0.05,
+ };
+ }
+
+ case 'sint': {
+ /* prettier-ignore */
+ const data = [
+ 42,
+ 0, 1, 2, 3, 4, 5,
+ -1, -2, -3, -4, -5,
+ Math.pow(2, bitSize - 2),
+ Math.pow(2, bitSize - 1) - 1, // max value
+ -Math.pow(2, bitSize - 2),
+ -Math.pow(2, bitSize - 1), // min value
+ ];
+ const expectedData = new Int32Array(data).buffer;
+ const vertexData =
+ bitSize === 32
+ ? expectedData
+ : bitSize === 16
+ ? new Int16Array(data).buffer
+ : new Int8Array(data).buffer;
+
+ return {
+ shaderBaseType: 'i32',
+ testComponentCount: data.length,
+ expectedData,
+ vertexData,
+ };
+ }
+
+ case 'uint': {
+ /* prettier-ignore */
+ const data = [
+ 42,
+ 0, 1, 2, 3, 4, 5,
+ Math.pow(2, bitSize - 1),
+ Math.pow(2, bitSize) - 1, // max value
+ ];
+ const expectedData = new Uint32Array(data).buffer;
+ const vertexData =
+ bitSize === 32
+ ? expectedData
+ : bitSize === 16
+ ? new Uint16Array(data).buffer
+ : new Uint8Array(data).buffer;
+
+ return {
+ shaderBaseType: 'u32',
+ testComponentCount: data.length,
+ expectedData,
+ vertexData,
+ };
+ }
+
+ case 'snorm': {
+ /* prettier-ignore */
+ const data = [
+ 42,
+ 0, 1, 2, 3, 4, 5,
+ -1, -2, -3, -4, -5,
+ Math.pow(2,bitSize - 2),
+ Math.pow(2,bitSize - 1) - 1, // max value
+ -Math.pow(2,bitSize - 2),
+ -Math.pow(2,bitSize - 1), // min value
+ ];
+ const vertexData =
+ bitSize === 16
+ ? new Int16Array(data).buffer
+ : bitSize === 8
+ ? new Int8Array(data).buffer
+ : unreachable();
+
+ return {
+ shaderBaseType: 'f32',
+ testComponentCount: data.length,
+ expectedData: new Float32Array(data.map(v => normalizedIntegerAsFloat(v, bitSize, true)))
+ .buffer,
+ vertexData,
+ floatTolerance: 0.1 * normalizedIntegerAsFloat(1, bitSize, true),
+ };
+ }
+
+ case 'unorm': {
+ /* prettier-ignore */
+ const data = [
+ 42,
+ 0, 1, 2, 3, 4, 5,
+ Math.pow(2, bitSize - 1),
+ Math.pow(2, bitSize) - 1, // max value
+ ];
+ const vertexData =
+ bitSize === 16
+ ? new Uint16Array(data).buffer
+ : bitSize === 8
+ ? new Uint8Array(data).buffer
+ : unreachable();
+
+ return {
+ shaderBaseType: 'f32',
+ testComponentCount: data.length,
+ expectedData: new Float32Array(data.map(v => normalizedIntegerAsFloat(v, bitSize, false)))
+ .buffer,
+ vertexData,
+ floatTolerance: 0.1 * normalizedIntegerAsFloat(1, bitSize, false),
+ };
+ }
+ }
+ }
+
+ // The TestData generated for a format might not contain enough data for all the vertices we are
+ // going to draw, so we expand them by adding additional copies of the vertexData as needed.
+ // expectedData is a bit different because it also needs to be unpacked to have `componentCount`
+ // components every 4 components (because the shader uses vec4 for the expected data).
+ expandTestData(data: TestData, maxCount: number, componentCount: number): TestData {
+ const vertexComponentSize = data.vertexData.byteLength / data.testComponentCount;
+ const expectedComponentSize = data.expectedData.byteLength / data.testComponentCount;
+
+ const expandedVertexData = new Uint8Array(maxCount * componentCount * vertexComponentSize);
+ const expandedExpectedData = new Uint8Array(4 * maxCount * expectedComponentSize);
+
+ for (let index = 0; index < maxCount; index++) {
+ for (let component = 0; component < componentCount; component++) {
+ // If only we had some builtin JS memcpy function between ArrayBuffers...
+ const targetVertexOffset = (index * componentCount + component) * vertexComponentSize;
+ const sourceVertexOffset = targetVertexOffset % data.vertexData.byteLength;
+ memcpy(
+ { src: data.vertexData, start: sourceVertexOffset, length: vertexComponentSize },
+ { dst: expandedVertexData, start: targetVertexOffset }
+ );
+
+ const targetExpectedOffset = (index * 4 + component) * expectedComponentSize;
+ const sourceExpectedOffset =
+ ((index * componentCount + component) * expectedComponentSize) %
+ data.expectedData.byteLength;
+ memcpy(
+ { src: data.expectedData, start: sourceExpectedOffset, length: expectedComponentSize },
+ { dst: expandedExpectedData, start: targetExpectedOffset }
+ );
+ }
+ }
+
+ return {
+ shaderBaseType: data.shaderBaseType,
+ testComponentCount: maxCount * componentCount,
+ floatTolerance: data.floatTolerance,
+ expectedData: expandedExpectedData.buffer,
+ vertexData: expandedVertexData.buffer,
+ };
+ }
+
+ // Copies `size` bytes from `source` to `target` starting at `offset` each `targetStride`.
+ // (the data in `source` is assumed packed)
+ interleaveVertexDataInto(
+ target: ArrayBuffer,
+ src: ArrayBuffer,
+ { targetStride, offset, size }: { targetStride: number; offset: number; size: number }
+ ) {
+ const dst = new Uint8Array(target);
+ for (
+ let srcStart = 0, dstStart = offset;
+ srcStart < src.byteLength;
+ srcStart += size, dstStart += targetStride
+ ) {
+ memcpy({ src, start: srcStart, length: size }, { dst, start: dstStart });
+ }
+ }
+
+ createTestAndPipelineData<V, A>(
+ state: VertexLayoutState<V, A>,
+ vertexCount: number,
+ instanceCount: number
+ ): VertexLayoutState<V, A & TestData> {
+ // Gather the test data and some additional test state for attribs.
+ return mapStateAttribs(state, (buffer, attrib) => {
+ const maxCount = buffer.stepMode === 'instance' ? instanceCount : vertexCount;
+ const formatInfo = kVertexFormatInfo[attrib.format];
+
+ let testData = this.generateTestData(attrib.format);
+ testData = this.expandTestData(testData, maxCount, formatInfo.componentCount);
+
+ return {
+ ...testData,
+ ...attrib,
+ };
+ });
+ }
+
+ createExpectedBG(state: VertexState<{}, TestData>, pipeline: GPURenderPipeline): GPUBindGroup {
+ // Create the bindgroups from that test data
+ const bgEntries: GPUBindGroupEntry[] = [];
+
+ for (const buffer of state) {
+ for (const attrib of buffer.attributes) {
+ const expectedDataBuffer = this.makeBufferWithContents(
+ new Uint8Array(attrib.expectedData),
+ GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE
+ );
+ bgEntries.push({
+ binding: attrib.shaderLocation,
+ resource: { buffer: expectedDataBuffer },
+ });
+ }
+ }
+
+ return this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: bgEntries,
+ });
+ }
+
+ createVertexBuffers(
+ state: VertexLayoutState<{ vbOffset?: number }, TestData>,
+ vertexCount: number,
+ instanceCount: number
+ ): VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}> {
+ // Create the vertex buffers
+ const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset?: number }, {}> = [];
+
+ for (const buffer of state) {
+ const maxCount = buffer.stepMode === 'instance' ? instanceCount : vertexCount;
+
+ // Fill the vertex data with garbage so that we don't get `0` (which could be a test value)
+ // if the vertex shader loads the vertex data incorrectly.
+ const vertexData = new ArrayBuffer(
+ align(buffer.arrayStride * maxCount + (buffer.vbOffset ?? 0), 4)
+ );
+ new Uint8Array(vertexData).fill(0xc4);
+
+ for (const attrib of buffer.attributes) {
+ const formatInfo = kVertexFormatInfo[attrib.format];
+ this.interleaveVertexDataInto(vertexData, attrib.vertexData, {
+ targetStride: buffer.arrayStride,
+ offset: (buffer.vbOffset ?? 0) + attrib.offset,
+ size: formatInfo.componentCount * formatInfo.bytesPerComponent,
+ });
+ }
+
+ vertexBuffers.push({
+ slot: buffer.slot,
+ buffer: this.makeBufferWithContents(new Uint8Array(vertexData), GPUBufferUsage.VERTEX),
+ vbOffset: buffer.vbOffset,
+ attributes: [],
+ });
+ }
+
+ return vertexBuffers;
+ }
+
+ runTest(
+ buffers: VertexLayoutState<{ vbOffset?: number }, { shaderComponentCount?: number }>,
+ // Default to using 20 vertices and 20 instances so that we cover each of the test data at least
+ // once (at the time of writing the largest testData has 16 values).
+ vertexCount: number = 20,
+ instanceCount: number = 20
+ ) {
+ const testData = this.createTestAndPipelineData(buffers, vertexCount, instanceCount);
+ const pipeline = this.makeTestPipeline(testData, vertexCount, instanceCount);
+ const expectedDataBG = this.createExpectedBG(testData, pipeline);
+ const vertexBuffers = this.createVertexBuffers(testData, vertexCount, instanceCount);
+ this.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, vertexCount, instanceCount);
+ }
+}
+
+export const g = makeTestGroup(VertexStateTest);
+
+g.test('vertex_format_to_shader_format_conversion')
+ .desc(
+ `Test that the raw data passed in vertex buffers is correctly converted to the input type in the shader. Test for:
+ - all formats
+ - 1 to 4 components in the shader's input type (unused components are filled with 0 and except the 4th with 1)
+ - various locations
+ - various slots`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .combine('shaderComponentCount', [1, 2, 3, 4])
+ .beginSubcases()
+ .combine('slot', [0, 1, kMaxVertexBuffers - 1])
+ .combine('shaderLocation', [0, 1, kMaxVertexAttributes - 1])
+ )
+ .fn(t => {
+ const { format, shaderComponentCount, slot, shaderLocation } = t.params;
+ t.runTest([
+ {
+ slot,
+ arrayStride: 16,
+ stepMode: 'vertex',
+ attributes: [
+ {
+ shaderLocation,
+ format,
+ offset: 0,
+ shaderComponentCount,
+ },
+ ],
+ },
+ ]);
+ });
+
+g.test('setVertexBuffer_offset_and_attribute_offset')
+ .desc(
+ `Test that the vertex buffer offset and attribute offset in the vertex state are applied correctly. Test for:
+ - all formats
+ - various setVertexBuffer offsets
+ - various attribute offsets in a fixed arrayStride`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('vbOffset', [0, 4, 400, 1004])
+ .combine('arrayStride', [128])
+ .expand('offset', p => {
+ const formatInfo = kVertexFormatInfo[p.format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ return new Set([
+ 0,
+ 4,
+ 8,
+ formatSize,
+ formatSize * 2,
+ p.arrayStride / 2,
+ p.arrayStride - formatSize - 4,
+ p.arrayStride - formatSize - 8,
+ p.arrayStride - formatSize - formatSize,
+ p.arrayStride - formatSize - formatSize * 2,
+ p.arrayStride - formatSize,
+ ]);
+ })
+ )
+ .fn(t => {
+ const { format, vbOffset, arrayStride, offset } = t.params;
+ t.runTest([
+ {
+ slot: 0,
+ arrayStride,
+ stepMode: 'vertex',
+ vbOffset,
+ attributes: [
+ {
+ shaderLocation: 0,
+ format,
+ offset,
+ },
+ ],
+ },
+ ]);
+ });
+
+g.test('non_zero_array_stride_and_attribute_offset')
+ .desc(
+ `Test that the array stride and attribute offset in the vertex state are applied correctly. Test for:
+ - all formats
+ - various array strides
+ - various attribute offsets in a fixed arrayStride`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .expand('arrayStride', p => {
+ const formatInfo = kVertexFormatInfo[p.format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+
+ return [align(formatSize, 4), align(formatSize, 4) + 4, kMaxVertexBufferArrayStride];
+ })
+ .expand('offset', p => {
+ const formatInfo = kVertexFormatInfo[p.format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ return new Set(
+ [
+ 0,
+ formatSize,
+ 4,
+ p.arrayStride / 2,
+ p.arrayStride - formatSize * 2,
+ p.arrayStride - formatSize - 4,
+ p.arrayStride - formatSize,
+ ].map(offset => clamp(offset, { min: 0, max: p.arrayStride - formatSize }))
+ );
+ })
+ )
+ .fn(t => {
+ const { format, arrayStride, offset } = t.params;
+ t.runTest([
+ {
+ slot: 0,
+ arrayStride,
+ stepMode: 'vertex',
+ attributes: [
+ {
+ shaderLocation: 0,
+ format,
+ offset,
+ },
+ ],
+ },
+ ]);
+ });
+
+g.test('buffers_with_varying_step_mode')
+ .desc(
+ `Test buffers with varying step modes in the same vertex state.
+ - Various combination of step modes`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('stepModes', [
+ ['instance'],
+ ['vertex', 'vertex', 'instance'],
+ ['instance', 'vertex', 'instance'],
+ ['vertex', 'instance', 'vertex', 'vertex'],
+ ])
+ )
+ .fn(t => {
+ const { stepModes } = t.params;
+ const state = (stepModes as GPUVertexStepMode[]).map((stepMode, i) => ({
+ slot: i,
+ arrayStride: 4,
+ stepMode,
+ attributes: [
+ {
+ shaderLocation: i,
+ format: 'float32' as const,
+ offset: 0,
+ },
+ ],
+ }));
+ t.runTest(state);
+ });
+
+g.test('vertex_buffer_used_multiple_times_overlapped')
+ .desc(
+ `Test using the same vertex buffer in for multiple "vertex buffers", with data from each buffer overlapping.
+ - For each vertex format.
+ - For various numbers of vertex buffers [2, 3, max]`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('vbCount', [2, 3, kMaxVertexBuffers])
+ .combine('additionalVBOffset', [0, 4, 120])
+ )
+ .fn(t => {
+ const { format, vbCount, additionalVBOffset } = t.params;
+ const kVertexCount = 20;
+ const kInstanceCount = 1;
+ const formatInfo = kVertexFormatInfo[format];
+ const formatByteSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ // We need to align so the offset for non-0 setVertexBuffer don't fail validation.
+ const alignedFormatByteSize = align(formatByteSize, 4);
+
+ // In this test we want to test using the same vertex buffer for multiple different attributes.
+ // For example if vbCount is 3, we will create a vertex buffer containing the following data:
+ // a0, a1, a2, a3, ..., a<baseDataVertexCount>
+ // We also create the expected data for the vertex fetching from that buffer so we can modify it
+ // below.
+ const baseDataVertexCount = kVertexCount + vbCount - 1;
+ const baseData = t.createTestAndPipelineData(
+ [
+ {
+ slot: 0,
+ arrayStride: alignedFormatByteSize,
+ stepMode: 'vertex',
+ vbOffset: additionalVBOffset,
+ attributes: [{ shaderLocation: 0, format, offset: 0 }],
+ },
+ ],
+ baseDataVertexCount,
+ kInstanceCount
+ );
+ const vertexBuffer = t.createVertexBuffers(baseData, baseDataVertexCount, kInstanceCount)[0]
+ .buffer;
+
+ // We are going to bind the vertex buffer multiple times, each time at a different offset that's
+ // a multiple of the data size. So what should be fetched by the vertex shader is:
+ // - attrib0: a0, a1, ..., a19
+ // - attrib1: a1, a2, ..., a20
+ // - attrib2: a2, a3, ..., a21
+ // etc.
+ // We re-create the test data by:
+ // 1) creating multiple "vertex buffers" that all point at the GPUBuffer above but at
+ // different offsets.
+ // 2) selecting what parts of the expectedData each attribute will see in the expectedData for
+ // the full vertex buffer.
+ const baseTestData = baseData[0].attributes[0];
+ assert(baseTestData.testComponentCount === formatInfo.componentCount * baseDataVertexCount);
+ const expectedDataBytesPerVertex = baseTestData.expectedData.byteLength / baseDataVertexCount;
+
+ const testData: VertexLayoutState<{}, TestData> = [];
+ const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset: number }, {}> = [];
+ for (let i = 0; i < vbCount; i++) {
+ vertexBuffers.push({
+ buffer: vertexBuffer,
+ slot: i,
+ vbOffset: additionalVBOffset + i * alignedFormatByteSize,
+ attributes: [],
+ });
+
+ testData.push({
+ slot: i,
+ arrayStride: alignedFormatByteSize,
+ stepMode: 'vertex',
+ attributes: [
+ {
+ shaderLocation: i,
+ format,
+ offset: 0,
+
+ shaderBaseType: baseTestData.shaderBaseType,
+ floatTolerance: baseTestData.floatTolerance,
+ // Select vertices [i, i + kVertexCount]
+ testComponentCount: kVertexCount * formatInfo.componentCount,
+ expectedData: baseTestData.expectedData.slice(
+ expectedDataBytesPerVertex * i,
+ expectedDataBytesPerVertex * (kVertexCount + i)
+ ),
+ vertexData: new ArrayBuffer(0),
+ },
+ ],
+ });
+ }
+
+ // Run the test with the modified test data.
+ const pipeline = t.makeTestPipeline(testData, kVertexCount, kInstanceCount);
+ const expectedDataBG = t.createExpectedBG(testData, pipeline);
+ t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kVertexCount, kInstanceCount);
+ });
+
+g.test('vertex_buffer_used_multiple_times_interleaved')
+ .desc(
+ `Test using the same vertex buffer in for multiple "vertex buffers", with data from each buffer interleaved.
+ - For each vertex format.
+ - For various numbers of vertex buffers [2, 3, max]`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('vbCount', [2, 3, kMaxVertexBuffers])
+ .combine('additionalVBOffset', [0, 4, 120])
+ )
+ .fn(t => {
+ const { format, vbCount, additionalVBOffset } = t.params;
+ const kVertexCount = 20;
+ const kInstanceCount = 1;
+ const formatInfo = kVertexFormatInfo[format];
+ const formatByteSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ // We need to align so the offset for non-0 setVertexBuffer don't fail validation.
+ const alignedFormatByteSize = align(formatByteSize, 4);
+
+ // Create data for a single vertex buffer with many attributes, that will be split between
+ // many vertex buffers set at different offsets.
+
+ // In this test we want to test using the same vertex buffer for multiple different attributes.
+ // For example if vbCount is 3, we will create a vertex buffer containing the following data:
+ // a0, a0, a0, a1, a1, a1, ...
+ // To do that we create a single vertex buffer with `vbCount` attributes that all have the same
+ // format.
+ const attribs: GPUVertexAttribute[] = [];
+ for (let i = 0; i < vbCount; i++) {
+ attribs.push({ format, offset: i * alignedFormatByteSize, shaderLocation: i });
+ }
+ const baseData = t.createTestAndPipelineData(
+ [
+ {
+ slot: 0,
+ arrayStride: alignedFormatByteSize * vbCount,
+ stepMode: 'vertex',
+ vbOffset: additionalVBOffset,
+ attributes: attribs,
+ },
+ ],
+ // Request one vertex more than what we need so we have an extra full stride. Otherwise WebGPU
+ // validation of vertex being in bounds will fail for all vertex buffers at an offset that's
+ // not 0 (since their last stride will go beyond the data for vertex kVertexCount -1).
+ kVertexCount + 1,
+ kInstanceCount
+ );
+ const vertexBuffer = t.createVertexBuffers(baseData, kVertexCount + 1, kInstanceCount)[0]
+ .buffer;
+
+ // Then we recreate test data by:
+ // 1) creating multiple "vertex buffers" that all point at the GPUBuffer above but at
+ // different offsets.
+ // 2) have multiple vertex buffer, each with one attributes that will expect a0, a1, ...
+ const testData: VertexLayoutState<{}, TestData> = [];
+ const vertexBuffers: VertexState<{ buffer: GPUBuffer; vbOffset: number }, {}> = [];
+ for (let i = 0; i < vbCount; i++) {
+ vertexBuffers.push({
+ slot: i,
+ buffer: vertexBuffer,
+ vbOffset: additionalVBOffset + i * alignedFormatByteSize,
+ attributes: [],
+ });
+ testData.push({
+ ...baseData[0],
+ slot: i,
+ attributes: [{ ...baseData[0].attributes[i], offset: 0 }],
+ });
+ }
+
+ // Run the test with the modified test data.
+ const pipeline = t.makeTestPipeline(testData, kVertexCount, kInstanceCount);
+ const expectedDataBG = t.createExpectedBG(testData, pipeline);
+ t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kVertexCount, kInstanceCount);
+ });
+
+g.test('max_buffers_and_attribs')
+ .desc(
+ `Test a vertex state that loads as many attributes and buffers as possible.
+ - For each format.
+ `
+ )
+ .params(u => u.combine('format', kVertexFormats))
+ .fn(t => {
+ const { format } = t.params;
+ const attributesPerBuffer = Math.ceil(kMaxVertexAttributes / kMaxVertexBuffers);
+ let attributesEmitted = 0;
+
+ const state: VertexLayoutState<{}, {}> = [];
+ for (let i = 0; i < kMaxVertexBuffers; i++) {
+ const attributes: GPUVertexAttribute[] = [];
+ for (let j = 0; j < attributesPerBuffer && attributesEmitted < kMaxVertexAttributes; j++) {
+ attributes.push({ format, offset: 0, shaderLocation: attributesEmitted });
+ attributesEmitted++;
+ }
+ state.push({
+ slot: i,
+ stepMode: 'vertex',
+ arrayStride: 32,
+ attributes,
+ });
+ }
+ t.runTest(state);
+ });
+
+g.test('array_stride_zero')
+ .desc(
+ `Test that arrayStride 0 correctly uses the same data for all vertex/instances, while another test vertex buffer with arrayStride != 0 gets different data.
+ - Test for all formats
+ - Test for both step modes`
+ )
+ .params(u =>
+ u //
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('stepMode', ['vertex', 'instance'] as const)
+ .expand('offset', p => {
+ const formatInfo = kVertexFormatInfo[p.format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ return new Set([
+ 0,
+ 4,
+ 8,
+ formatSize,
+ formatSize * 2,
+ kMaxVertexBufferArrayStride / 2,
+ kMaxVertexBufferArrayStride - formatSize - 4,
+ kMaxVertexBufferArrayStride - formatSize - 8,
+ kMaxVertexBufferArrayStride - formatSize,
+ kMaxVertexBufferArrayStride - formatSize * 2,
+ ]);
+ })
+ )
+ .fn(t => {
+ const { format, stepMode, offset } = t.params;
+ const kCount = 10;
+
+ // Create the stride 0 part of the test, first by faking a single vertex being drawn and
+ // then expanding the data to cover kCount vertex / instances
+ const stride0TestData = t.createTestAndPipelineData(
+ [
+ {
+ slot: 0,
+ arrayStride: 2048,
+ stepMode,
+ vbOffset: offset, // used to push data in the vertex buffer
+ attributes: [{ format, offset: 0, shaderLocation: 0 }],
+ },
+ ],
+ 1,
+ 1
+ )[0];
+ const stride0VertexBuffer = t.createVertexBuffers([stride0TestData], kCount, kCount)[0];
+
+ // Expand the stride0 test data to have kCount values for expectedData.
+ const originalData = stride0TestData.attributes[0].expectedData;
+ const expandedData = new ArrayBuffer(kCount * originalData.byteLength);
+ for (let i = 0; i < kCount; i++) {
+ new Uint8Array(expandedData, originalData.byteLength * i).set(new Uint8Array(originalData));
+ }
+
+ // Fixup stride0TestData to use arrayStride 0.
+ stride0TestData.attributes[0].offset = offset;
+ stride0TestData.attributes[0].expectedData = expandedData;
+ stride0TestData.attributes[0].testComponentCount *= kCount;
+ stride0TestData.arrayStride = 0;
+ stride0VertexBuffer.vbOffset = 0;
+
+ // Create the part of the state that will be varying for each vertex / instance
+ const varyingTestData = t.createTestAndPipelineData(
+ [
+ {
+ slot: 1,
+ arrayStride: 32,
+ stepMode,
+ attributes: [{ format, offset: 0, shaderLocation: 1 }],
+ },
+ ],
+ kCount,
+ kCount
+ )[0];
+ const varyingVertexBuffer = t.createVertexBuffers([varyingTestData], kCount, kCount)[0];
+
+ // Run the test with the merged test state.
+ const state = [stride0TestData, varyingTestData];
+ const vertexBuffers = [stride0VertexBuffer, varyingVertexBuffer];
+
+ const pipeline = t.makeTestPipeline(state, kCount, kCount);
+ const expectedDataBG = t.createExpectedBG(state, pipeline);
+ t.submitRenderPass(pipeline, vertexBuffers, expectedDataBG, kCount, kCount);
+ });
+
+g.test('discontiguous_location_and_attribs')
+ .desc('Test that using far away slots / shaderLocations works as expected')
+ .fn(t => {
+ t.runTest([
+ {
+ slot: kMaxVertexBuffers - 1,
+ arrayStride: 4,
+ stepMode: 'vertex',
+ attributes: [
+ { format: 'uint8x2', offset: 2, shaderLocation: 0 },
+ { format: 'uint8x2', offset: 0, shaderLocation: 8 },
+ ],
+ },
+ {
+ slot: 1,
+ arrayStride: 16,
+ stepMode: 'instance',
+ vbOffset: 1000,
+ attributes: [{ format: 'uint32x4', offset: 0, shaderLocation: kMaxVertexAttributes - 1 }],
+ },
+ ]);
+ });
+
+g.test('overlapping_attributes')
+ .desc(
+ `Test that overlapping attributes in the same vertex buffer works
+ - Test for all formats`
+ )
+ .params(u => u.combine('format', kVertexFormats))
+ .fn(t => {
+ const { format } = t.params;
+
+ const attributes: GPUVertexAttribute[] = [];
+ for (let i = 0; i < kMaxVertexAttributes; i++) {
+ attributes.push({ format, offset: 0, shaderLocation: i });
+ }
+
+ t.runTest([
+ {
+ slot: 0,
+ stepMode: 'vertex',
+ arrayStride: 32,
+ attributes,
+ },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/index_format.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/index_format.spec.ts
new file mode 100644
index 0000000000..3815e0cd57
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/vertex_state/index_format.spec.ts
@@ -0,0 +1,584 @@
+export const description = `
+Test indexing, index format and primitive restart.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { getTextureCopyLayout } from '../../../util/texture/layout.js';
+
+const kHeight = 4;
+const kWidth = 8;
+const kTextureFormat = 'r8uint' as const;
+
+/** 4x4 grid of r8uint values (each 0 or 1). */
+type Raster8x4 = readonly [
+ readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
+ readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
+ readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1],
+ readonly [0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1]
+];
+
+/** Expected 4x4 rasterization of a bottom-left triangle. */
+const kBottomLeftTriangle: Raster8x4 = [
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 1, 1, 0, 0],
+ [0, 0, 0, 0, 1, 1, 1, 0],
+];
+
+/** Expected 4x4 rasterization filling the whole quad. */
+const kSquare: Raster8x4 = [
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+];
+
+/** Expected 4x4 rasterization with no pixels. */
+const kNothing: Raster8x4 = [
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+];
+
+const { byteLength, bytesPerRow, rowsPerImage } = getTextureCopyLayout(kTextureFormat, '2d', [
+ kWidth,
+ kHeight,
+ 1,
+]);
+
+class IndexFormatTest extends GPUTest {
+ MakeRenderPipeline(
+ topology: GPUPrimitiveTopology,
+ stripIndexFormat?: GPUIndexFormat
+ ): GPURenderPipeline {
+ const vertexModule = this.device.createShaderModule({
+ // NOTE: These positions will create triangles that cut right through pixel centers. If this
+ // results in different rasterization results on different hardware, tweak to avoid this.
+ code: `
+ @vertex
+ fn main(@builtin(vertex_index) VertexIndex : u32)
+ -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 4>(
+ vec2<f32>(0.01, 0.98),
+ vec2<f32>(0.99, -0.98),
+ vec2<f32>(0.99, 0.98),
+ vec2<f32>(0.01, -0.98));
+
+ if (VertexIndex == 0xFFFFu || VertexIndex == 0xFFFFFFFFu) {
+ return vec4<f32>(-0.99, -0.98, 0.0, 1.0);
+ }
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ }
+ `,
+ });
+
+ const fragmentModule = this.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) u32 {
+ return 1u;
+ }
+ `,
+ });
+
+ return this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [] }),
+ vertex: { module: vertexModule, entryPoint: 'main' },
+ fragment: {
+ module: fragmentModule,
+ entryPoint: 'main',
+ targets: [{ format: kTextureFormat }],
+ },
+ primitive: {
+ topology,
+ stripIndexFormat,
+ },
+ });
+ }
+
+ CreateIndexBuffer(indices: readonly number[], indexFormat: GPUIndexFormat): GPUBuffer {
+ const typedArrayConstructor = { uint16: Uint16Array, uint32: Uint32Array }[indexFormat];
+ return this.makeBufferWithContents(new typedArrayConstructor(indices), GPUBufferUsage.INDEX);
+ }
+
+ run(
+ indexBuffer: GPUBuffer,
+ indexCount: number,
+ indexFormat: GPUIndexFormat,
+ indexOffset: number = 0,
+ primitiveTopology: GPUPrimitiveTopology = 'triangle-list'
+ ): GPUBuffer {
+ let pipeline: GPURenderPipeline;
+ // The indexFormat must be set in render pipeline descriptor that specifies a strip primitive
+ // topology for primitive restart testing
+ if (primitiveTopology === 'line-strip' || primitiveTopology === 'triangle-strip') {
+ pipeline = this.MakeRenderPipeline(primitiveTopology, indexFormat);
+ } else {
+ pipeline = this.MakeRenderPipeline(primitiveTopology);
+ }
+
+ const colorAttachment = this.device.createTexture({
+ format: kTextureFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const result = this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
+ pass.drawIndexed(indexCount);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment },
+ { buffer: result, bytesPerRow, rowsPerImage },
+ [kWidth, kHeight]
+ );
+ this.device.queue.submit([encoder.finish()]);
+
+ return result;
+ }
+
+ CreateExpectedUint8Array(renderShape: Raster8x4): Uint8Array {
+ const arrayBuffer = new Uint8Array(byteLength);
+ for (let row = 0; row < renderShape.length; row++) {
+ for (let col = 0; col < renderShape[row].length; col++) {
+ const texel: 0 | 1 = renderShape[row][col];
+
+ const kBytesPerTexel = 1; // r8uint
+ const byteOffset = row * bytesPerRow + col * kBytesPerTexel;
+ arrayBuffer[byteOffset] = texel;
+ }
+ }
+ return arrayBuffer;
+ }
+}
+
+export const g = makeTestGroup(IndexFormatTest);
+
+g.test('index_format,uint16')
+ .desc('Test rendering result of indexed draw with index format of uint16.')
+ .paramsSubcasesOnly([
+ { indexOffset: 0, _indexCount: 10, _expectedShape: kSquare },
+ { indexOffset: 6, _indexCount: 6, _expectedShape: kBottomLeftTriangle },
+ { indexOffset: 18, _indexCount: 0, _expectedShape: kNothing },
+ ])
+ .fn(t => {
+ const { indexOffset, _indexCount, _expectedShape } = t.params;
+
+ // If this is written as uint16 but interpreted as uint32, it will have index 1 and 2 be both 0
+ // and render nothing.
+ // And the index buffer size - offset must be not less than the size required by triangle
+ // list, otherwise it also render nothing.
+ const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
+ const indexBuffer = t.CreateIndexBuffer(indices, 'uint16');
+ const result = t.run(indexBuffer, _indexCount, 'uint16', indexOffset);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(_expectedShape);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
+
+g.test('index_format,uint32')
+ .desc('Test rendering result of indexed draw with index format of uint32.')
+ .paramsSubcasesOnly([
+ { indexOffset: 0, _indexCount: 10, _expectedShape: kSquare },
+ { indexOffset: 12, _indexCount: 7, _expectedShape: kBottomLeftTriangle },
+ { indexOffset: 36, _indexCount: 0, _expectedShape: kNothing },
+ ])
+ .fn(t => {
+ const { indexOffset, _indexCount, _expectedShape } = t.params;
+
+ // If this is interpreted as uint16, then it would be 0, 1, 0, ... and would draw nothing.
+ // And the index buffer size - offset must be not less than the size required by triangle
+ // list, otherwise it also render nothing.
+ const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
+ const indexBuffer = t.CreateIndexBuffer(indices, 'uint32');
+ const result = t.run(indexBuffer, _indexCount, 'uint32', indexOffset);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(_expectedShape);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
+
+g.test('index_format,change_pipeline_after_setIndexBuffer')
+ .desc('Test that setting the index buffer before the pipeline works correctly.')
+ .params(u => u.combine('setPipelineBeforeSetIndexBuffer', [false, true]))
+ .fn(t => {
+ const indexOffset = 12;
+ const indexCount = 7;
+ const expectedShape = kBottomLeftTriangle;
+
+ const indexFormat16 = 'uint16';
+ const indexFormat32 = 'uint32';
+
+ const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
+ const indexBuffer = t.CreateIndexBuffer(indices, indexFormat32);
+
+ const kPrimitiveTopology = 'triangle-strip';
+ const pipeline32 = t.MakeRenderPipeline(kPrimitiveTopology, indexFormat32);
+ const pipeline16 = t.MakeRenderPipeline(kPrimitiveTopology, indexFormat16);
+
+ const colorAttachment = t.device.createTexture({
+ format: kTextureFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const result = t.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ if (t.params.setPipelineBeforeSetIndexBuffer) {
+ pass.setPipeline(pipeline16);
+ }
+ pass.setIndexBuffer(indexBuffer, indexFormat32, indexOffset);
+ pass.setPipeline(pipeline32); // Set the pipeline for 'indexFormat32' again.
+ pass.drawIndexed(indexCount);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment },
+ { buffer: result, bytesPerRow, rowsPerImage },
+ [kWidth, kHeight]
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(expectedShape);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
+
+g.test('index_format,setIndexBuffer_before_setPipeline')
+ .desc('Test that setting the index buffer before the pipeline works correctly.')
+ .params(u => u.combine('setIndexBufferBeforeSetPipeline', [false, true]))
+ .fn(t => {
+ const indexOffset = 12;
+ const indexCount = 7;
+ const expectedShape = kBottomLeftTriangle;
+
+ const indexFormat = 'uint32';
+
+ const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
+ const indexBuffer = t.CreateIndexBuffer(indices, indexFormat);
+
+ const kPrimitiveTopology = 'triangle-strip';
+ const pipeline = t.MakeRenderPipeline(kPrimitiveTopology, indexFormat);
+
+ const colorAttachment = t.device.createTexture({
+ format: kTextureFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const result = t.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ if (t.params.setIndexBufferBeforeSetPipeline) {
+ pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
+ pass.setPipeline(pipeline);
+ } else {
+ pass.setPipeline(pipeline);
+ pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
+ }
+
+ pass.drawIndexed(indexCount);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment },
+ { buffer: result, bytesPerRow, rowsPerImage },
+ [kWidth, kHeight]
+ );
+ t.device.queue.submit([encoder.finish()]);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(expectedShape);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
+
+g.test('index_format,setIndexBuffer_different_formats')
+ .desc(
+ `
+ Test that index buffers of multiple formats can be used with a pipeline that doesn't use strip
+ primitive topology.
+ `
+ )
+ .fn(t => {
+ const indices: number[] = [1, 2, 0, 0, 0, 0, 0, 1, 3, 0];
+
+ // Create a pipeline to be used by different index formats.
+ const kPrimitiveTopology = 'triangle-list';
+ const pipeline = t.MakeRenderPipeline(kPrimitiveTopology);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(kBottomLeftTriangle);
+
+ const colorAttachment = t.device.createTexture({
+ format: kTextureFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const result = t.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ let encoder = t.device.createCommandEncoder();
+ {
+ const indexFormat = 'uint32';
+ const indexOffset = 12;
+ const indexCount = 7;
+ const indexBuffer = t.CreateIndexBuffer(indices, indexFormat);
+
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
+ pass.setPipeline(pipeline);
+ pass.drawIndexed(indexCount);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment },
+ { buffer: result, bytesPerRow, rowsPerImage },
+ [kWidth, kHeight]
+ );
+ }
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+
+ // Call setIndexBuffer with the pipeline and a different index format buffer.
+ encoder = t.device.createCommandEncoder();
+ {
+ const indexFormat = 'uint16';
+ const indexOffset = 6;
+ const indexCount = 6;
+ const indexBuffer = t.CreateIndexBuffer(indices, indexFormat);
+
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ pass.setIndexBuffer(indexBuffer, indexFormat, indexOffset);
+ pass.setPipeline(pipeline);
+ pass.drawIndexed(indexCount);
+ pass.end();
+ encoder.copyTextureToBuffer(
+ { texture: colorAttachment },
+ { buffer: result, bytesPerRow, rowsPerImage },
+ [kWidth, kHeight]
+ );
+ }
+ t.device.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
+
+g.test('primitive_restart')
+ .desc(
+ `
+Test primitive restart with each primitive topology.
+
+Primitive restart should be always active with strip primitive topologies
+('line-strip' or 'triangle-strip') and never active for other topologies, where
+the primitive restart value isn't special and should be treated as a regular index value.
+
+The value -1 gets uploaded as 0xFFFF or 0xFFFF_FFFF according to the format.
+
+The positions of these points are embedded in the shader above, and look like this:
+ | 0 2|
+ | |
+ -1 3 1|
+
+Below are the indices lists used for each test, and the expected rendering result of each
+(approximately, in the case of incorrect results). This shows the expected result (marked '->')
+is different from what you would get if the topology were incorrect.
+
+- primitiveTopology: triangle-list
+ indices: [0, 1, 3, -1, 2, 1, 0, 0],
+ -> triangle-list: (0, 1, 3), (-1, 2, 1)
+ | # #|
+ | ####|
+ | #####|
+ | #######|
+ triangle-list with restart: (0, 1, 3), (2, 1, 0)
+ triangle-strip: (0, 1, 3), (2, 1, 0), (1, 0, 0)
+ | ####|
+ | ####|
+ | ####|
+ | ####|
+ triangle-strip w/o restart: (0, 1, 3), (1, 3, -1), (3, -1, 2), (-1, 2, 1), (2, 1, 0), (1, 0, 0)
+ | ####|
+ | ####|
+ | #####|
+ | #######|
+
+- primitiveTopology: triangle-strip
+ indices: [3, 1, 0, -1, 2, 2, 1, 3],
+ -> triangle-strip: (3, 1, 0), (2, 2, 1), (2, 1, 3)
+ | # #|
+ | ####|
+ | ####|
+ | ####|
+ triangle-strip w/o restart: (3, 1, 0), (1, 0, -1), (0, -1, 2), (2, 2, 1), (2, 3, 1)
+ | ####|
+ | #####|
+ | ######|
+ | #######|
+ triangle-list: (3, 1, 0), (-1, 2, 2)
+ triangle-list with restart: (3, 1, 0), (2, 2, 1)
+ | |
+ | # |
+ | ## |
+ | ### |
+
+- primitiveTopology: point, line-list, line-strip:
+ indices: [0, 1, -1, 2, -1, 2, 3, 0],
+ -> point-list: (0), (1), (-1), (2), (3), (0)
+ | # #|
+ | |
+ | |
+ |# # #|
+ point-list with restart (0), (1), (2), (3), (0)
+ | # #|
+ | |
+ | |
+ | # #|
+ -> line-list: (0, 1), (-1, 2), (3, 0)
+ | # ##|
+ | ## |
+ | ### # |
+ |## # #|
+ line-list with restart: (0, 1), (2, 3)
+ | # #|
+ | ## |
+ | ## |
+ | # #|
+ -> line-strip: (0, 1), (2, 3), (3, 0)
+ | # #|
+ | ### |
+ | ### |
+ | # #|
+ line-strip w/o restart: (0, 1), (1, -1), (-1, 2), (2, 3), (3, 3)
+ | # ##|
+ | ### |
+ | ## ## |
+ |########|
+`
+ )
+ .params(u =>
+ u //
+ .combine('indexFormat', ['uint16', 'uint32'] as const)
+ .combineWithParams([
+ {
+ primitiveTopology: 'point-list',
+ _indices: [0, 1, -1, 2, 3, 0],
+ _expectedShape: [
+ [0, 0, 0, 0, 1, 0, 0, 1],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 0, 1, 0, 0, 1],
+ ],
+ },
+ {
+ primitiveTopology: 'line-list',
+ _indices: [0, 1, -1, 2, 3, 0],
+ _expectedShape: [
+ [0, 0, 0, 0, 1, 0, 1, 1],
+ [0, 0, 0, 0, 1, 1, 0, 0],
+ [0, 0, 1, 1, 1, 0, 1, 0],
+ [1, 1, 0, 0, 1, 0, 0, 1],
+ ],
+ },
+ {
+ primitiveTopology: 'line-strip',
+ _indices: [0, 1, -1, 2, 3, 0],
+ _expectedShape: [
+ [0, 0, 0, 0, 1, 0, 0, 1],
+ [0, 0, 0, 0, 1, 1, 1, 0],
+ [0, 0, 0, 0, 1, 1, 1, 0],
+ [0, 0, 0, 0, 1, 0, 0, 1],
+ ],
+ },
+ {
+ primitiveTopology: 'triangle-list',
+ _indices: [0, 1, 3, -1, 2, 1, 0, 0],
+ _expectedShape: [
+ [0, 0, 0, 0, 0, 0, 0, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ [0, 0, 0, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1],
+ ],
+ },
+ {
+ primitiveTopology: 'triangle-strip',
+ _indices: [3, 1, 0, -1, 2, 2, 1, 3],
+ _expectedShape: [
+ [0, 0, 0, 0, 0, 0, 0, 1],
+ [0, 0, 0, 0, 1, 0, 1, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 1, 1, 1, 1],
+ ],
+ },
+ ] as const)
+ )
+ .fn(t => {
+ const { indexFormat, primitiveTopology, _indices, _expectedShape } = t.params;
+
+ const indexBuffer = t.CreateIndexBuffer(_indices, indexFormat);
+ const result = t.run(indexBuffer, _indices.length, indexFormat, 0, primitiveTopology);
+
+ const expectedTextureValues = t.CreateExpectedUint8Array(_expectedShape);
+ t.expectGPUBufferValuesEqual(result, expectedTextureValues);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/regression/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/regression/README.txt
new file mode 100644
index 0000000000..263b04a372
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/regression/README.txt
@@ -0,0 +1,2 @@
+One-off tests that reproduce API bugs found in implementations to prevent the bugs from
+appearing again.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/README.txt
new file mode 100644
index 0000000000..8bf08d7b08
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/README.txt
@@ -0,0 +1 @@
+Positive and negative tests for all the validation rules of the API.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/create.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/create.spec.ts
new file mode 100644
index 0000000000..2766d40530
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/create.spec.ts
@@ -0,0 +1,121 @@
+export const description = `
+Tests for validation in createBuffer.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import {
+ kAllBufferUsageBits,
+ kBufferSizeAlignment,
+ kBufferUsages,
+ kLimitInfo,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { kMaxSafeMultipleOf8 } from '../../../util/math.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+assert(kBufferSizeAlignment === 4);
+g.test('size')
+ .desc(
+ 'Test buffer size alignment is validated to be a multiple of 4 if mappedAtCreation is true.'
+ )
+ .params(u =>
+ u
+ .combine('mappedAtCreation', [false, true])
+ .beginSubcases()
+ .combine('size', [
+ 0,
+ kBufferSizeAlignment * 0.5,
+ kBufferSizeAlignment,
+ kBufferSizeAlignment * 1.5,
+ kBufferSizeAlignment * 2,
+ ])
+ )
+ .fn(t => {
+ const { mappedAtCreation, size } = t.params;
+ const isValid = !mappedAtCreation || size % kBufferSizeAlignment === 0;
+ const usage = BufferUsage.COPY_SRC;
+ t.expectGPUError(
+ 'validation',
+ () => t.device.createBuffer({ size, usage, mappedAtCreation }),
+ !isValid
+ );
+ });
+
+g.test('limit')
+ .desc('Test buffer size is validated against maxBufferSize.')
+ .params(u =>
+ u
+ .beginSubcases()
+ .combine('size', [
+ kLimitInfo.maxBufferSize.default - 1,
+ kLimitInfo.maxBufferSize.default,
+ kLimitInfo.maxBufferSize.default + 1,
+ ])
+ )
+ .fn(t => {
+ const { size } = t.params;
+ const isValid = size <= kLimitInfo.maxBufferSize.default;
+ const usage = BufferUsage.COPY_SRC;
+ t.expectGPUError('validation', () => t.device.createBuffer({ size, usage }), !isValid);
+ });
+
+const kInvalidUsage = 0x8000;
+assert((kInvalidUsage & kAllBufferUsageBits) === 0);
+g.test('usage')
+ .desc('Test combinations of zero to two usage flags are validated to be valid.')
+ .params(u =>
+ u
+ .combine('usage1', [0, ...kBufferUsages, kInvalidUsage])
+ .combine('usage2', [0, ...kBufferUsages, kInvalidUsage])
+ .beginSubcases()
+ .combine('mappedAtCreation', [false, true])
+ )
+ .fn(t => {
+ const { mappedAtCreation, usage1, usage2 } = t.params;
+ const usage = usage1 | usage2;
+
+ const isValid =
+ usage !== 0 &&
+ (usage & ~kAllBufferUsageBits) === 0 &&
+ ((usage & GPUBufferUsage.MAP_READ) === 0 ||
+ (usage & ~(GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ)) === 0) &&
+ ((usage & GPUBufferUsage.MAP_WRITE) === 0 ||
+ (usage & ~(GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE)) === 0);
+
+ t.expectGPUError(
+ 'validation',
+ () => t.device.createBuffer({ size: kBufferSizeAlignment * 2, usage, mappedAtCreation }),
+ !isValid
+ );
+ });
+
+const BufferUsage = GPUConst.BufferUsage;
+
+g.test('createBuffer_invalid_and_oom')
+ .desc(
+ `When creating a mappable buffer, it's expected that shmem may be immediately allocated
+(in the content process, before validation occurs in the GPU process). If the buffer is really
+large, though, it could fail shmem allocation before validation fails. Ensure that OOM error is
+hidden behind the "more severe" validation error.`
+ )
+ .paramsSubcasesOnly(u =>
+ u.combineWithParams([
+ { _valid: true, usage: BufferUsage.UNIFORM, size: 16 },
+ { _valid: true, usage: BufferUsage.STORAGE, size: 16 },
+ // Invalid because UNIFORM is not allowed with map usages.
+ { usage: BufferUsage.MAP_WRITE | BufferUsage.UNIFORM, size: 16 },
+ { usage: BufferUsage.MAP_WRITE | BufferUsage.UNIFORM, size: kMaxSafeMultipleOf8 },
+ { usage: BufferUsage.MAP_WRITE | BufferUsage.UNIFORM, size: 0x20_0000_0000 }, // 128 GiB
+ { usage: BufferUsage.MAP_READ | BufferUsage.UNIFORM, size: 16 },
+ { usage: BufferUsage.MAP_READ | BufferUsage.UNIFORM, size: kMaxSafeMultipleOf8 },
+ { usage: BufferUsage.MAP_READ | BufferUsage.UNIFORM, size: 0x20_0000_0000 }, // 128 GiB
+ ] as const)
+ )
+ .fn(t => {
+ const { _valid, usage, size } = t.params;
+
+ t.expectGPUError('validation', () => t.device.createBuffer({ size, usage }), !_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/destroy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/destroy.spec.ts
new file mode 100644
index 0000000000..e9297d699c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/destroy.spec.ts
@@ -0,0 +1,101 @@
+export const description = `
+Validation tests for GPUBuffer.destroy.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kBufferUsages } from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('all_usages')
+ .desc('Test destroying buffers of every usage type.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('usage', kBufferUsages)
+ )
+ .fn(async t => {
+ const { usage } = t.params;
+ const buf = t.device.createBuffer({
+ size: 4,
+ usage,
+ });
+
+ buf.destroy();
+ });
+
+g.test('error_buffer')
+ .desc('Test that error buffers may be destroyed without generating validation errors.')
+ .fn(async t => {
+ const buf = t.getErrorBuffer();
+ buf.destroy();
+ });
+
+g.test('twice')
+ .desc(
+ `Test that destroying a buffer more than once is allowed.
+ - Tests buffers which are mapped at creation or not
+ - Tests buffers with various usages`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('mappedAtCreation', [false, true])
+ .combineWithParams([
+ { size: 4, usage: GPUConst.BufferUsage.COPY_SRC },
+ { size: 4, usage: GPUConst.BufferUsage.MAP_WRITE | GPUConst.BufferUsage.COPY_SRC },
+ { size: 4, usage: GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ },
+ ])
+ )
+ .fn(async t => {
+ const buf = t.device.createBuffer(t.params);
+
+ buf.destroy();
+ buf.destroy();
+ });
+
+g.test('while_mapped')
+ .desc(
+ `Test destroying buffers while mapped or after being unmapped.
+ - Tests {mappable, unmappable mapAtCreation, mappable mapAtCreation}
+ - Tests while {mapped, mapped at creation, unmapped}`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('mappedAtCreation', [false, true])
+ .combine('unmapBeforeDestroy', [false, true])
+ .combineWithParams([
+ { usage: GPUConst.BufferUsage.COPY_SRC },
+ { usage: GPUConst.BufferUsage.MAP_WRITE | GPUConst.BufferUsage.COPY_SRC },
+ { usage: GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ },
+ {
+ usage: GPUConst.BufferUsage.MAP_WRITE | GPUConst.BufferUsage.COPY_SRC,
+ mapMode: GPUConst.MapMode.WRITE,
+ },
+ {
+ usage: GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ,
+ mapMode: GPUConst.MapMode.READ,
+ },
+ ])
+ .unless(p => p.mappedAtCreation === false && p.mapMode === undefined)
+ )
+ .fn(async t => {
+ const { usage, mapMode, mappedAtCreation, unmapBeforeDestroy } = t.params;
+ const buf = t.device.createBuffer({
+ size: 4,
+ usage,
+ mappedAtCreation,
+ });
+
+ if (mapMode !== undefined) {
+ if (mappedAtCreation) {
+ buf.unmap();
+ }
+ await buf.mapAsync(mapMode);
+ }
+ if (unmapBeforeDestroy) {
+ buf.unmap();
+ }
+
+ buf.destroy();
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts
new file mode 100644
index 0000000000..61cc1c4e14
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/mapping.spec.ts
@@ -0,0 +1,1124 @@
+export const description = `
+Validation tests for GPUBuffer.mapAsync, GPUBuffer.unmap and GPUBuffer.getMappedRange.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { attemptGarbageCollection } from '../../../../common/util/collect_garbage.js';
+import { assert, unreachable } from '../../../../common/util/util.js';
+import { kBufferUsages } from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ async testMapAsyncCall(
+ expectation:
+ | 'success'
+ | { validationError: boolean; earlyRejection: boolean; rejectName: string },
+ buffer: GPUBuffer,
+ mode: GPUMapModeFlags,
+ offset?: number,
+ size?: number
+ ) {
+ if (expectation === 'success') {
+ const p = buffer.mapAsync(mode, offset, size);
+ await p;
+ } else {
+ let p: Promise<void>;
+ this.expectValidationError(() => {
+ p = buffer.mapAsync(mode, offset, size);
+ }, expectation.validationError);
+ let caught = false;
+ let rejectedEarly = false;
+ // If mapAsync rejected early, microtask A will run before B.
+ // If not, B will run before A.
+ p!.catch(() => {
+ // Microtask A
+ caught = true;
+ });
+ queueMicrotask(() => {
+ // Microtask B
+ rejectedEarly = caught;
+ });
+ try {
+ // This await will always complete after microtasks A and B are both done.
+ await p!;
+ assert(expectation.rejectName === null, 'mapAsync unexpectedly passed');
+ } catch (ex) {
+ assert(ex instanceof Error, 'mapAsync rejected with non-error');
+ assert(expectation.rejectName === ex.name, `mapAsync rejected unexpectedly with: ${ex}`);
+ assert(
+ expectation.earlyRejection === rejectedEarly,
+ 'mapAsync rejected at an unexpected timing'
+ );
+ }
+ }
+ }
+
+ testGetMappedRangeCall(success: boolean, buffer: GPUBuffer, offset?: number, size?: number) {
+ if (success) {
+ const data = buffer.getMappedRange(offset, size);
+ this.expect(data instanceof ArrayBuffer);
+ if (size !== undefined) {
+ this.expect(data.byteLength === size);
+ }
+ } else {
+ this.shouldThrow('OperationError', () => {
+ buffer.getMappedRange(offset, size);
+ });
+ }
+ }
+
+ createMappableBuffer(type: GPUMapModeFlags, size: number): GPUBuffer {
+ switch (type) {
+ case GPUMapMode.READ:
+ return this.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.MAP_READ,
+ });
+ case GPUMapMode.WRITE:
+ return this.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.MAP_WRITE,
+ });
+ default:
+ unreachable();
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kMapModeOptions = [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE];
+const kOffsetAlignment = 8;
+const kSizeAlignment = 4;
+
+g.test('mapAsync,usage')
+ .desc(
+ `Test the usage validation for mapAsync.
+
+ For each buffer usage:
+ For GPUMapMode.READ, GPUMapMode.WRITE, and 0:
+ Test that the mapAsync call is valid iff the mapping usage is not 0 and the buffer usage
+ the mapMode flag.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([
+ { mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
+ { mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
+ // Using mapMode 0 is never valid, so there is no validUsage.
+ { mapMode: 0, validUsage: null },
+ ])
+ .combine('usage', kBufferUsages)
+ )
+ .fn(async t => {
+ const { mapMode, validUsage, usage } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const successParam =
+ usage === validUsage
+ ? 'success'
+ : {
+ validationError: true,
+ earlyRejection: false,
+ rejectName: 'OperationError',
+ };
+ await t.testMapAsyncCall(successParam, buffer, mapMode);
+ });
+
+g.test('mapAsync,invalidBuffer')
+ .desc('Test that mapAsync is an error when called on an invalid buffer.')
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.getErrorBuffer();
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+ });
+
+g.test('mapAsync,state,destroyed')
+ .desc('Test that mapAsync is an error when called on a destroyed buffer.')
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ // Start mapping the buffer, we are going to destroy it before it resolves so it will reject
+ // the mapping promise with an AbortError.
+ const pending = t.testMapAsyncCall(
+ { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
+ buffer,
+ mapMode
+ );
+
+ buffer.destroy();
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ await pending;
+ });
+
+g.test('mapAsync,state,mappedAtCreation')
+ .desc(
+ `Test that mapAsync is an error when called on a buffer mapped at creation,
+ but succeeds after unmapping it.`
+ )
+ .paramsSubcasesOnly([
+ { mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
+ { mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
+ ])
+ .fn(async t => {
+ const { mapMode, validUsage } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: validUsage,
+ mappedAtCreation: true,
+ });
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ buffer.unmap();
+ await t.testMapAsyncCall('success', buffer, mapMode);
+ });
+
+g.test('mapAsync,state,mapped')
+ .desc(
+ `Test that mapAsync is an error when called on a mapped buffer, but succeeds
+ after unmapping it.`
+ )
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+
+ const buffer = t.createMappableBuffer(mapMode, 16);
+ await t.testMapAsyncCall('success', buffer, mapMode);
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ buffer.unmap();
+ await t.testMapAsyncCall('success', buffer, mapMode);
+ });
+
+g.test('mapAsync,state,mappingPending')
+ .desc(
+ `Test that mapAsync is rejected when called on a buffer that is being mapped,
+ but succeeds after the previous mapping request is cancelled.`
+ )
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ // Start mapping the buffer, we are going to unmap it before it resolves so it will reject
+ // the mapping promise with an AbortError.
+ const pending0 = t.testMapAsyncCall(
+ { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
+ buffer,
+ mapMode
+ );
+
+ // Do the test of mapAsync while [[pending_map]] is non-null. It has to be synchronous so
+ // that we can unmap the previous mapping in the same stack frame and testing this one doesn't
+ // get canceled, but instead is rejected.
+ const pending1 = t.testMapAsyncCall(
+ { validationError: false, earlyRejection: true, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ // Unmap the first mapping. It should now be possible to successfully call mapAsync
+ // This unmap should cause the first mapAsync rejection.
+ buffer.unmap();
+ await t.testMapAsyncCall('success', buffer, mapMode);
+
+ await pending0;
+ await pending1;
+ });
+
+g.test('mapAsync,sizeUnspecifiedOOB')
+ .desc(
+ `Test that mapAsync with size unspecified rejects if offset > buffer.[[size]],
+ with various cases at the limits of the buffer size or with a misaligned offset.
+ Also test for an empty buffer.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('mapMode', kMapModeOptions)
+ .combineWithParams([
+ // 0 size buffer.
+ { bufferSize: 0, offset: 0 },
+ { bufferSize: 0, offset: 1 },
+ { bufferSize: 0, offset: kOffsetAlignment },
+
+ // Test with a buffer that's not empty.
+ { bufferSize: 16, offset: 0 },
+ { bufferSize: 16, offset: kOffsetAlignment },
+ { bufferSize: 16, offset: 16 },
+ { bufferSize: 16, offset: 17 },
+ { bufferSize: 16, offset: 16 + kOffsetAlignment },
+ ])
+ )
+ .fn(async t => {
+ const { mapMode, bufferSize, offset } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+
+ const successParam =
+ offset <= bufferSize
+ ? 'success'
+ : {
+ validationError: true,
+ earlyRejection: false,
+ rejectName: 'OperationError',
+ };
+ await t.testMapAsyncCall(successParam, buffer, mapMode, offset);
+ });
+
+g.test('mapAsync,offsetAndSizeAlignment')
+ .desc("Test that mapAsync fails if the alignment of offset and size isn't correct.")
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('mapMode', kMapModeOptions)
+ .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
+ .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
+ )
+ .fn(async t => {
+ const { mapMode, offset, size } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ const successParam =
+ offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0
+ ? 'success'
+ : {
+ validationError: true,
+ earlyRejection: false,
+ rejectName: 'OperationError',
+ };
+ await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
+ });
+
+g.test('mapAsync,offsetAndSizeOOB')
+ .desc('Test that mapAsync fails if offset + size is larger than the buffer size.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('mapMode', kMapModeOptions)
+ .combineWithParams([
+ // For a 0 size buffer
+ { bufferSize: 0, offset: 0, size: 0 },
+ { bufferSize: 0, offset: 0, size: 4 },
+ { bufferSize: 0, offset: 8, size: 0 },
+
+ // For a small buffer
+ { bufferSize: 16, offset: 0, size: 16 },
+ { bufferSize: 16, offset: kOffsetAlignment, size: 16 },
+
+ { bufferSize: 16, offset: 16, size: 0 },
+ { bufferSize: 16, offset: 16, size: kSizeAlignment },
+
+ { bufferSize: 16, offset: 8, size: 0 },
+ { bufferSize: 16, offset: 8, size: 8 },
+ { bufferSize: 16, offset: 8, size: 8 + kSizeAlignment },
+
+ // For a larger buffer
+ { bufferSize: 1024, offset: 0, size: 1024 },
+ { bufferSize: 1024, offset: kOffsetAlignment, size: 1024 },
+
+ { bufferSize: 1024, offset: 1024, size: 0 },
+ { bufferSize: 1024, offset: 1024, size: kSizeAlignment },
+
+ { bufferSize: 1024, offset: 512, size: 0 },
+ { bufferSize: 1024, offset: 512, size: 512 },
+ { bufferSize: 1024, offset: 512, size: 512 + kSizeAlignment },
+ ])
+ )
+ .fn(async t => {
+ const { mapMode, bufferSize, size, offset } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+
+ const successParam =
+ offset + size <= bufferSize
+ ? 'success'
+ : {
+ validationError: true,
+ earlyRejection: false,
+ rejectName: 'OperationError',
+ };
+ await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
+ });
+
+g.test('mapAsync,earlyRejection')
+ .desc("Test that mapAsync fails immediately if it's pending map.")
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions).combine('offset2', [0, 8]))
+ .fn(async t => {
+ const { mapMode, offset2 } = t.params;
+
+ const bufferSize = 16;
+ const mapSize = 8;
+ const offset1 = 0;
+
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+ const p1 = buffer.mapAsync(mapMode, offset1, mapSize); // succeeds
+ await t.testMapAsyncCall(
+ {
+ validationError: false,
+ earlyRejection: true,
+ rejectName: 'OperationError',
+ },
+ buffer,
+ mapMode,
+ offset2,
+ mapSize
+ );
+ await p1; // ensure the original map still succeeds
+ });
+
+g.test('mapAsync,abort_over_invalid_error')
+ .desc(
+ `Test that unmap abort error should have precedence over validation error
+TODO
+ - Add other validation error test (eg. offset is not a multiple of 8)
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('mapMode', kMapModeOptions).combine('unmapBeforeResolve', [true, false])
+ )
+ .fn(async t => {
+ const { mapMode, unmapBeforeResolve } = t.params;
+ const bufferSize = 8;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+ await buffer.mapAsync(mapMode);
+
+ if (unmapBeforeResolve) {
+ // unmap abort error should have precedence over validation error
+ const pending = t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'AbortError' },
+ buffer,
+ mapMode
+ );
+ buffer.unmap();
+ await pending;
+ } else {
+ // map on already mapped buffer should cause validation error
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+ buffer.unmap();
+ }
+ });
+
+g.test('getMappedRange,state,mapped')
+ .desc('Test that it is valid to call getMappedRange in the mapped state')
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const bufferSize = 16;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+ await buffer.mapAsync(mapMode);
+
+ const data = buffer.getMappedRange();
+ t.expect(data instanceof ArrayBuffer);
+ t.expect(data.byteLength === bufferSize);
+
+ // map on already mapped buffer should be rejected
+ const pending = t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+ t.expect(data.byteLength === bufferSize);
+ await pending;
+
+ buffer.unmap();
+
+ t.expect(data.byteLength === 0);
+ });
+
+g.test('getMappedRange,state,mappedAtCreation')
+ .desc(
+ `Test that, in the mapped-at-creation state, it is valid to call getMappedRange, for all buffer usages,
+ and invalid to call mapAsync, for all map modes.`
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('bufferUsage', kBufferUsages).combine('mapMode', kMapModeOptions)
+ )
+ .fn(async t => {
+ const { bufferUsage, mapMode } = t.params;
+ const bufferSize = 16;
+ const buffer = t.device.createBuffer({
+ usage: bufferUsage,
+ size: bufferSize,
+ mappedAtCreation: true,
+ });
+
+ const data = buffer.getMappedRange();
+ t.expect(data instanceof ArrayBuffer);
+ t.expect(data.byteLength === bufferSize);
+
+ // map on already mapped buffer should be rejected
+ const pending = t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+ t.expect(data.byteLength === bufferSize);
+ await pending;
+
+ buffer.unmap();
+
+ t.expect(data.byteLength === 0);
+ });
+
+g.test('getMappedRange,state,invalid_mappedAtCreation')
+ .desc(
+ `mappedAtCreation should return a mapped buffer, even if the buffer is invalid.
+Like VRAM allocation (see map_oom), validation can be performed asynchronously (in the GPU process)
+so the Content process doesn't necessarily know the buffer is invalid.`
+ )
+ .fn(async t => {
+ const buffer = t.expectGPUError('validation', () =>
+ t.device.createBuffer({
+ mappedAtCreation: true,
+ size: 16,
+ usage: 0xffff_ffff, // Invalid usage
+ })
+ );
+
+ // Should still be valid.
+ buffer.getMappedRange();
+ });
+
+g.test('getMappedRange,state,mappedAgain')
+ .desc(
+ 'Test that it is valid to call getMappedRange in the mapped state, even if there is a duplicate mapAsync before'
+ )
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+ await buffer.mapAsync(mapMode);
+
+ // call mapAsync again on already mapped buffer should fail
+ await t.testMapAsyncCall(
+ { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ // getMapppedRange should still success
+ t.testGetMappedRangeCall(true, buffer);
+ });
+
+g.test('getMappedRange,state,unmapped')
+ .desc(
+ `Test that it is invalid to call getMappedRange in the unmapped state.
+Test for various cases of being unmapped: at creation, after a mapAsync call or after being created mapped.`
+ )
+ .fn(async t => {
+ // It is invalid to call getMappedRange when the buffer starts unmapped when created.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ t.testGetMappedRangeCall(false, buffer);
+ }
+
+ // It is invalid to call getMappedRange when the buffer is unmapped after mapAsync.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ await buffer.mapAsync(GPUMapMode.READ);
+ buffer.unmap();
+ t.testGetMappedRangeCall(false, buffer);
+ }
+
+ // It is invalid to call getMappedRange when the buffer is unmapped after mappedAtCreation.
+ {
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.MAP_READ,
+ size: 16,
+ mappedAtCreation: true,
+ });
+ buffer.unmap();
+ t.testGetMappedRangeCall(false, buffer);
+ }
+ });
+
+g.test('getMappedRange,subrange,mapped')
+ .desc(
+ `Test that old getMappedRange returned arraybuffer does not exist after unmap, and newly returned
+ arraybuffer after new map has correct subrange`
+ )
+ .params(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const bufferSize = 16;
+ const offset = 8;
+ const subrangeSize = bufferSize - offset;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+ await buffer.mapAsync(mapMode);
+
+ const data0 = buffer.getMappedRange();
+ t.expect(data0 instanceof ArrayBuffer);
+ t.expect(data0.byteLength === bufferSize);
+
+ buffer.unmap();
+ t.expect(data0.byteLength === 0);
+
+ await buffer.mapAsync(mapMode, offset);
+ const data1 = buffer.getMappedRange(8);
+
+ t.expect(data0.byteLength === 0);
+ t.expect(data1.byteLength === subrangeSize);
+ });
+
+g.test('getMappedRange,subrange,mappedAtCreation')
+ .desc(
+ `Test that old getMappedRange returned arraybuffer does not exist after unmap and newly returned
+ arraybuffer after new map has correct subrange`
+ )
+ .fn(async t => {
+ const bufferSize = 16;
+ const offset = 8;
+ const subrangeSize = bufferSize - offset;
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ mappedAtCreation: true,
+ });
+
+ const data0 = buffer.getMappedRange();
+ t.expect(data0 instanceof ArrayBuffer);
+ t.expect(data0.byteLength === bufferSize);
+
+ buffer.unmap();
+ t.expect(data0.byteLength === 0);
+
+ await buffer.mapAsync(GPUMapMode.READ, offset);
+ const data1 = buffer.getMappedRange(8);
+
+ t.expect(data0.byteLength === 0);
+ t.expect(data1.byteLength === subrangeSize);
+ });
+
+g.test('getMappedRange,state,destroyed')
+ .desc(
+ `Test that it is invalid to call getMappedRange in the destroyed state.
+Test for various cases of being destroyed: at creation, after a mapAsync call or after being created mapped.`
+ )
+ .fn(async t => {
+ // It is invalid to call getMappedRange when the buffer is destroyed when unmapped.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ buffer.destroy();
+ t.testGetMappedRangeCall(false, buffer);
+ }
+
+ // It is invalid to call getMappedRange when the buffer is destroyed when mapped.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ await buffer.mapAsync(GPUMapMode.READ);
+ buffer.destroy();
+ t.testGetMappedRangeCall(false, buffer);
+ }
+
+ // It is invalid to call getMappedRange when the buffer is destroyed when mapped at creation.
+ {
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.MAP_READ,
+ size: 16,
+ mappedAtCreation: true,
+ });
+ buffer.destroy();
+ t.testGetMappedRangeCall(false, buffer);
+ }
+ });
+
+g.test('getMappedRange,state,mappingPending')
+ .desc(`Test that it is invalid to call getMappedRange in the mappingPending state.`)
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ /* noawait */ const mapping0 = buffer.mapAsync(mapMode);
+ // seconding mapping should be rejected
+ const mapping1 = t.testMapAsyncCall(
+ { validationError: false, earlyRejection: true, rejectName: 'OperationError' },
+ buffer,
+ mapMode
+ );
+
+ // invalid in mappingPending state
+ t.testGetMappedRangeCall(false, buffer);
+
+ await mapping0;
+
+ // valid after buffer is mapped
+ t.testGetMappedRangeCall(true, buffer);
+
+ await mapping1;
+ });
+
+g.test('getMappedRange,offsetAndSizeAlignment,mapped')
+ .desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
+ .params(u =>
+ u
+ .combine('mapMode', kMapModeOptions)
+ .beginSubcases()
+ .combine('mapOffset', [0, kOffsetAlignment])
+ .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
+ .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
+ )
+ .fn(async t => {
+ const { mapMode, mapOffset, offset, size } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 32);
+ await buffer.mapAsync(mapMode, mapOffset);
+
+ const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
+ t.testGetMappedRangeCall(success, buffer, offset + mapOffset, size);
+ });
+
+g.test('getMappedRange,offsetAndSizeAlignment,mappedAtCreation')
+ .desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
+ .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
+ )
+ .fn(async t => {
+ const { offset, size } = t.params;
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ mappedAtCreation: true,
+ });
+ const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
+ t.testGetMappedRangeCall(success, buffer, offset, size);
+ });
+
+g.test('getMappedRange,sizeAndOffsetOOB,mappedAtCreation')
+ .desc(
+ `Test that getMappedRange size + offset must be less than the buffer size for a
+ buffer mapped at creation. (and offset has not constraints on its own)`
+ )
+ .paramsSubcasesOnly([
+ // Tests for a zero-sized buffer, with and without a size defined.
+ { bufferSize: 0, offset: undefined, size: undefined },
+ { bufferSize: 0, offset: undefined, size: 0 },
+ { bufferSize: 0, offset: undefined, size: kSizeAlignment },
+ { bufferSize: 0, offset: 0, size: undefined },
+ { bufferSize: 0, offset: 0, size: 0 },
+ { bufferSize: 0, offset: kOffsetAlignment, size: undefined },
+ { bufferSize: 0, offset: kOffsetAlignment, size: 0 },
+
+ // Tests for a non-empty buffer, with an undefined offset.
+ { bufferSize: 80, offset: undefined, size: 80 },
+ { bufferSize: 80, offset: undefined, size: 80 + kSizeAlignment },
+
+ // Tests for a non-empty buffer, with an undefined size.
+ { bufferSize: 80, offset: undefined, size: undefined },
+ { bufferSize: 80, offset: 0, size: undefined },
+ { bufferSize: 80, offset: kOffsetAlignment, size: undefined },
+ { bufferSize: 80, offset: 80, size: undefined },
+ { bufferSize: 80, offset: 80 + kOffsetAlignment, size: undefined },
+
+ // Tests for a non-empty buffer with a size defined.
+ { bufferSize: 80, offset: 0, size: 80 },
+ { bufferSize: 80, offset: 0, size: 80 + kSizeAlignment },
+ { bufferSize: 80, offset: kOffsetAlignment, size: 80 },
+
+ { bufferSize: 80, offset: 40, size: 40 },
+ { bufferSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
+ { bufferSize: 80, offset: 40, size: 40 + kSizeAlignment },
+ ])
+ .fn(t => {
+ const { bufferSize, offset, size } = t.params;
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_DST,
+ mappedAtCreation: true,
+ });
+
+ const actualOffset = offset ?? 0;
+ const actualSize = size ?? bufferSize - actualOffset;
+
+ const success = actualOffset <= bufferSize && actualOffset + actualSize <= bufferSize;
+ t.testGetMappedRangeCall(success, buffer, offset, size);
+ });
+
+g.test('getMappedRange,sizeAndOffsetOOB,mapped')
+ .desc('Test that getMappedRange size + offset must be less than the mapAsync range.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('mapMode', kMapModeOptions)
+ .combineWithParams([
+ // Tests for an empty buffer, and implicit mapAsync size.
+ { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: undefined },
+ { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: 0 },
+ {
+ bufferSize: 0,
+ mapOffset: 0,
+ mapSize: undefined,
+ offset: undefined,
+ size: kSizeAlignment,
+ },
+ { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: undefined },
+ { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: 0 },
+ {
+ bufferSize: 0,
+ mapOffset: 0,
+ mapSize: undefined,
+ offset: kOffsetAlignment,
+ size: undefined,
+ },
+ { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: kOffsetAlignment, size: 0 },
+
+ // Tests for an empty buffer, and explicit mapAsync size.
+ { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: undefined, size: undefined },
+ { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: undefined },
+ { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: 0 },
+ { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: undefined },
+ { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: 0 },
+
+ // Test for a fully implicit mapAsync call
+ { bufferSize: 80, mapOffset: undefined, mapSize: undefined, offset: 0, size: 80 },
+ {
+ bufferSize: 80,
+ mapOffset: undefined,
+ mapSize: undefined,
+ offset: 0,
+ size: 80 + kSizeAlignment,
+ },
+ {
+ bufferSize: 80,
+ mapOffset: undefined,
+ mapSize: undefined,
+ offset: kOffsetAlignment,
+ size: 80,
+ },
+
+ // Test for a mapAsync call with an implicit size
+ { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: 80 - 24 },
+ {
+ bufferSize: 80,
+ mapOffset: 24,
+ mapSize: undefined,
+ offset: 0,
+ size: 80 - 24 + kSizeAlignment,
+ },
+ {
+ bufferSize: 80,
+ mapOffset: 24,
+ mapSize: undefined,
+ offset: kOffsetAlignment,
+ size: 80 - 24,
+ },
+
+ // Test for a non-empty buffer fully mapped.
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 },
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: kOffsetAlignment, size: 80 },
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 + kSizeAlignment },
+
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 },
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
+ { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 + kSizeAlignment },
+
+ // Test for a buffer partially mapped.
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 },
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 - kOffsetAlignment, size: 40 },
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 + kOffsetAlignment, size: 40 },
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 + kSizeAlignment },
+
+ // Test for a partially mapped buffer with implicit size and offset for getMappedRange.
+ // - Buffer partially mapped in the middle
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: undefined, size: undefined },
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 0, size: undefined },
+ { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: undefined },
+ // - Buffer partially mapped to the end
+ { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: undefined },
+ { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 80, size: undefined },
+ // - Buffer partially mapped from the start
+ { bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: undefined },
+ { bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: 64 },
+ ])
+ )
+ .fn(async t => {
+ const { mapMode, bufferSize, mapOffset, mapSize, offset, size } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, bufferSize);
+ await buffer.mapAsync(mapMode, mapOffset, mapSize);
+
+ const actualMapOffset = mapOffset ?? 0;
+ const actualMapSize = mapSize ?? bufferSize - actualMapOffset;
+
+ const actualOffset = offset ?? 0;
+ const actualSize = size ?? bufferSize - actualOffset;
+
+ const success =
+ actualOffset >= actualMapOffset &&
+ actualOffset <= bufferSize &&
+ actualOffset + actualSize <= actualMapOffset + actualMapSize;
+ t.testGetMappedRangeCall(success, buffer, offset, size);
+ });
+
+g.test('getMappedRange,disjointRanges')
+ .desc('Test that the ranges asked through getMappedRange must be disjoint.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('remapBetweenCalls', [false, true])
+ .combineWithParams([
+ // Disjoint ranges with one that's empty.
+ { offset1: 8, size1: 0, offset2: 8, size2: 8 },
+ { offset1: 16, size1: 0, offset2: 8, size2: 8 },
+
+ { offset1: 8, size1: 8, offset2: 8, size2: 0 },
+ { offset1: 8, size1: 8, offset2: 16, size2: 0 },
+
+ // Disjoint ranges with both non-empty.
+ { offset1: 0, size1: 8, offset2: 8, size2: 8 },
+ { offset1: 16, size1: 8, offset2: 8, size2: 8 },
+
+ { offset1: 8, size1: 8, offset2: 0, size2: 8 },
+ { offset1: 8, size1: 8, offset2: 16, size2: 8 },
+
+ // Empty range contained inside another one.
+ { offset1: 16, size1: 20, offset2: 24, size2: 0 },
+ { offset1: 24, size1: 0, offset2: 16, size2: 20 },
+
+ // Ranges that overlap only partially.
+ { offset1: 16, size1: 20, offset2: 8, size2: 20 },
+ { offset1: 16, size1: 20, offset2: 32, size2: 20 },
+
+ // Ranges that include one another.
+ { offset1: 0, size1: 80, offset2: 16, size2: 20 },
+ { offset1: 16, size1: 20, offset2: 0, size2: 80 },
+ ])
+ )
+ .fn(async t => {
+ const { offset1, size1, offset2, size2, remapBetweenCalls } = t.params;
+ const buffer = t.device.createBuffer({ size: 80, usage: GPUBufferUsage.MAP_READ });
+ await buffer.mapAsync(GPUMapMode.READ);
+
+ t.testGetMappedRangeCall(true, buffer, offset1, size1);
+
+ if (remapBetweenCalls) {
+ buffer.unmap();
+ await buffer.mapAsync(GPUMapMode.READ);
+ }
+
+ const range1StartsAfter2 = offset1 >= offset2 + size2;
+ const range2StartsAfter1 = offset2 >= offset1 + size1;
+ const disjoint = range1StartsAfter2 || range2StartsAfter1;
+ const success = disjoint || remapBetweenCalls;
+
+ t.testGetMappedRangeCall(success, buffer, offset2, size2);
+ });
+
+g.test('getMappedRange,disjoinRanges_many')
+ .desc('Test getting a lot of small ranges, and that the disjoint check checks them all.')
+ .fn(async t => {
+ const kStride = 256;
+ const kNumStrides = 256;
+
+ const buffer = t.device.createBuffer({
+ size: kStride * kNumStrides,
+ usage: GPUBufferUsage.MAP_READ,
+ });
+ await buffer.mapAsync(GPUMapMode.READ);
+
+ // Get a lot of small mapped ranges.
+ for (let stride = 0; stride < kNumStrides; stride++) {
+ t.testGetMappedRangeCall(true, buffer, stride * kStride, 8);
+ }
+
+ // Check for each range it is invalid to get a range that overlaps it and check that it is valid
+ // to get ranges for the rest of the buffer.
+ for (let stride = 0; stride < kNumStrides; stride++) {
+ t.testGetMappedRangeCall(false, buffer, stride * kStride, kStride);
+ t.testGetMappedRangeCall(true, buffer, stride * kStride + 8, kStride - 8);
+ }
+ });
+
+g.test('unmap,state,unmapped')
+ .desc(
+ `Test it is valid to call unmap on a buffer that is unmapped (at creation, or after
+ mappedAtCreation or mapAsync)`
+ )
+ .fn(async t => {
+ // It is valid to call unmap after creation of an unmapped buffer.
+ {
+ const buffer = t.device.createBuffer({ size: 16, usage: GPUBufferUsage.MAP_READ });
+ buffer.unmap();
+ }
+
+ // It is valid to call unmap after unmapping a mapAsynced buffer.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ await buffer.mapAsync(GPUMapMode.READ);
+ buffer.unmap();
+ buffer.unmap();
+ }
+
+ // It is valid to call unmap after unmapping a mappedAtCreation buffer.
+ {
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.MAP_READ,
+ size: 16,
+ mappedAtCreation: true,
+ });
+ buffer.unmap();
+ buffer.unmap();
+ }
+ });
+
+g.test('unmap,state,destroyed')
+ .desc(
+ `Test it is valid to call unmap on a buffer that is destroyed (at creation, or after
+ mappedAtCreation or mapAsync)`
+ )
+ .fn(async t => {
+ // It is valid to call unmap after destruction of an unmapped buffer.
+ {
+ const buffer = t.device.createBuffer({ size: 16, usage: GPUBufferUsage.MAP_READ });
+ buffer.destroy();
+ buffer.unmap();
+ }
+
+ // It is valid to call unmap after destroying a mapAsynced buffer.
+ {
+ const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
+ await buffer.mapAsync(GPUMapMode.READ);
+ buffer.destroy();
+ buffer.unmap();
+ }
+
+ // It is valid to call unmap after destroying a mappedAtCreation buffer.
+ {
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.MAP_READ,
+ size: 16,
+ mappedAtCreation: true,
+ });
+ buffer.destroy();
+ buffer.unmap();
+ }
+ });
+
+g.test('unmap,state,mappedAtCreation')
+ .desc('Test it is valid to call unmap on a buffer mapped at creation, for various usages')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('bufferUsage', kBufferUsages)
+ )
+ .fn(t => {
+ const { bufferUsage } = t.params;
+ const buffer = t.device.createBuffer({ size: 16, usage: bufferUsage, mappedAtCreation: true });
+
+ buffer.unmap();
+ });
+
+g.test('unmap,state,mapped')
+ .desc("Test it is valid to call unmap on a buffer that's mapped")
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ await buffer.mapAsync(mapMode);
+ buffer.unmap();
+ });
+
+g.test('unmap,state,mappingPending')
+ .desc("Test it is valid to call unmap on a buffer that's being mapped")
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+ const buffer = t.createMappableBuffer(mapMode, 16);
+
+ const pending = t.testMapAsyncCall(
+ { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
+ buffer,
+ mapMode
+ );
+ buffer.unmap();
+ await pending;
+ });
+
+g.test('gc_behavior,mappedAtCreation')
+ .desc(
+ "Test that GCing the buffer while mappings are handed out doesn't invalidate them - mappedAtCreation case"
+ )
+ .fn(async t => {
+ let buffer = null;
+ buffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.COPY_DST,
+ mappedAtCreation: true,
+ });
+
+ // Write some non-zero data to the buffer.
+ const contents = new Uint32Array(buffer.getMappedRange());
+ for (let i = 0; i < contents.length; i++) {
+ contents[i] = i;
+ }
+
+ // Trigger garbage collection that should collect the buffer (or as if it collected it)
+ // NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
+ // back to the OS. One good option for browsers to check their logic is good is to zero-out the
+ // memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
+ buffer = null;
+ await attemptGarbageCollection();
+
+ // Use the mapping again both for read and write, it should work.
+ for (let i = 0; i < contents.length; i++) {
+ t.expect(contents[i] === i);
+ contents[i] = i + 1;
+ }
+ });
+
+g.test('gc_behavior,mapAsync')
+ .desc(
+ "Test that GCing the buffer while mappings are handed out doesn't invalidate them - mapAsync case"
+ )
+ .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
+ .fn(async t => {
+ const { mapMode } = t.params;
+
+ let buffer = null;
+ buffer = t.createMappableBuffer(mapMode, 256);
+ await buffer.mapAsync(mapMode);
+
+ // Write some non-zero data to the buffer.
+ const contents = new Uint32Array(buffer.getMappedRange());
+ for (let i = 0; i < contents.length; i++) {
+ contents[i] = i;
+ }
+
+ // Trigger garbage collection that should collect the buffer (or as if it collected it)
+ // NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
+ // back to the OS. One good option for browsers to check their logic is good is to zero-out the
+ // memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
+ buffer = null;
+ await attemptGarbageCollection();
+
+ // Use the mapping again both for read and write, it should work.
+ for (let i = 0; i < contents.length; i++) {
+ t.expect(contents[i] === i);
+ contents[i] = i + 1;
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/threading.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/threading.spec.ts
new file mode 100644
index 0000000000..b449b36d25
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/buffer/threading.spec.ts
@@ -0,0 +1,14 @@
+export const description = `
+TODO:
+- Try to map on one thread while {pending, mapped, mappedAtCreation, mappedAtCreation+unmap+mapped}
+ on another thread.
+- Invalid to postMessage a mapped range's ArrayBuffer or ArrayBufferView
+ {with, without} it being in the transfer array.
+- Copy GPUBuffer to another thread while {pending, mapped mappedAtCreation} on {same,diff} thread
+ (valid), then try to map on that thread (invalid)
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/README.txt
new file mode 100644
index 0000000000..608e66d18f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/README.txt
@@ -0,0 +1,10 @@
+Test every method or option that shouldn't be allowed without a feature enabled.
+If the feature is not enabled, any use of an enum value added by a feature must be an
+*exception*, per <https://github.com/gpuweb/gpuweb/blob/main/design/ErrorConventions.md>.
+
+- x= that feature {enabled, disabled}
+
+Generally one file for each feature name, but some may be grouped (e.g. one file for all optional
+query types, one file for all optional texture formats).
+
+TODO: implement
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts
new file mode 100644
index 0000000000..c773737c64
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/query_types.spec.ts
@@ -0,0 +1,76 @@
+export const description = `
+Tests for capability checking for features enabling optional query types.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('createQuerySet')
+ .desc(
+ `
+ Tests that creating a query set throws a type error exception if the features don't contain
+ 'timestamp-query'.
+ - createQuerySet
+ - type {occlusion, timestamp}
+ - x= {pipeline statistics, timestamp} query {enable, disable}
+ `
+ )
+ .params(u =>
+ u
+ .combine('type', ['occlusion', 'timestamp'] as const)
+ .combine('featureContainsTimestampQuery', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const { featureContainsTimestampQuery } = t.params;
+
+ const requiredFeatures: GPUFeatureName[] = [];
+ if (featureContainsTimestampQuery) {
+ requiredFeatures.push('timestamp-query');
+ }
+
+ t.selectDeviceOrSkipTestCase({ requiredFeatures });
+ })
+ .fn(async t => {
+ const { type, featureContainsTimestampQuery } = t.params;
+
+ const count = 1;
+ const shouldException = type === 'timestamp' && !featureContainsTimestampQuery;
+
+ t.shouldThrow(shouldException ? 'TypeError' : false, () => {
+ t.device.createQuerySet({ type, count });
+ });
+ });
+
+g.test('writeTimestamp')
+ .desc(
+ `
+ Tests that writing a timestamp throws a type error exception if the features don't contain
+ 'timestamp-query'.
+ `
+ )
+ .params(u => u.combine('featureContainsTimestampQuery', [false, true]))
+ .beforeAllSubcases(t => {
+ const { featureContainsTimestampQuery } = t.params;
+
+ const requiredFeatures: GPUFeatureName[] = [];
+ if (featureContainsTimestampQuery) {
+ requiredFeatures.push('timestamp-query');
+ }
+
+ t.selectDeviceOrSkipTestCase({ requiredFeatures });
+ })
+ .fn(async t => {
+ const { featureContainsTimestampQuery } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: featureContainsTimestampQuery ? 'timestamp' : 'occlusion',
+ count: 1,
+ });
+ const encoder = t.createEncoder('non-pass');
+
+ t.shouldThrow(featureContainsTimestampQuery ? false : 'TypeError', () => {
+ encoder.encoder.writeTimestamp(querySet, 0);
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts
new file mode 100644
index 0000000000..717a6e2d27
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/features/texture_formats.spec.ts
@@ -0,0 +1,445 @@
+export const description = `
+Tests for capability checking for features enabling optional texture formats.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { assert } from '../../../../../common/util/util.js';
+import { kAllTextureFormats, kTextureFormatInfo } from '../../../../capability_info.js';
+import { kAllCanvasTypes, createCanvas } from '../../../../util/create_elements.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+const kOptionalTextureFormats = kAllTextureFormats.filter(
+ t => kTextureFormatInfo[t].feature !== undefined
+);
+
+g.test('texture_descriptor')
+ .desc(
+ `
+ Test creating a texture with an optional texture format will fail if the required optional feature
+ is not enabled.
+ `
+ )
+ .params(u =>
+ u.combine('format', kOptionalTextureFormats).combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createTexture({
+ format,
+ size: [formatInfo.blockWidth, formatInfo.blockHeight, 1] as const,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+ });
+ });
+
+g.test('texture_descriptor_view_formats')
+ .desc(
+ `
+ Test creating a texture with view formats that have an optional texture format will fail if the
+ required optional feature is not enabled.
+ `
+ )
+ .params(u =>
+ u.combine('format', kOptionalTextureFormats).combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createTexture({
+ format,
+ size: [formatInfo.blockWidth, formatInfo.blockHeight, 1] as const,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ viewFormats: [format],
+ });
+ });
+ });
+
+g.test('texture_view_descriptor')
+ .desc(
+ `
+ Test creating a texture view with all texture formats will fail if the required optional feature
+ is not enabled.
+ `
+ )
+ .params(u =>
+ u.combine('format', kOptionalTextureFormats).combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ // If the required feature isn't enabled then the texture will fail to create and we won't be
+ // able to test createView, so pick and alternate guaranteed format instead. This will almost
+ // certainly not be view-compatible with the format being tested, but that doesn't matter since
+ // createView should throw an exception due to the format feature not being enabled before it
+ // has a chance to validate that the view and texture formats aren't compatible.
+ const textureFormat = enable_required_feature ? format : 'rgba8unorm';
+
+ const formatInfo = kTextureFormatInfo[format];
+ const testTexture = t.device.createTexture({
+ format: textureFormat,
+ size: [formatInfo.blockWidth, formatInfo.blockHeight, 1] as const,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+ const testViewDesc: GPUTextureViewDescriptor = {
+ format,
+ dimension: '2d',
+ aspect: 'all',
+ arrayLayerCount: 1,
+ baseMipLevel: 0,
+ mipLevelCount: 1,
+ baseArrayLayer: 0,
+ };
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ testTexture.createView(testViewDesc);
+ });
+ });
+
+g.test('canvas_configuration')
+ .desc(
+ `
+ Test configuring a canvas with optional texture formats will throw an exception if the required
+ optional feature is not enabled. Otherwise, a validation error should be generated instead of
+ throwing an exception.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, canvasType, enable_required_feature } = t.params;
+
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ const canvasConf = {
+ device: t.device,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ };
+
+ if (enable_required_feature) {
+ t.expectValidationError(() => {
+ ctx.configure(canvasConf);
+ });
+ } else {
+ t.shouldThrow('TypeError', () => {
+ ctx.configure(canvasConf);
+ });
+ }
+ });
+
+g.test('canvas_configuration_view_formats')
+ .desc(
+ `
+ Test that configuring a canvas with view formats throws an exception if the required optional
+ feature is not enabled. Otherwise, a validation error should be generated instead of throwing an
+ exception.
+ `
+ )
+ .params(u =>
+ u
+ .combine('viewFormats', [
+ ...kOptionalTextureFormats.map(format => [format]),
+ ['bgra8unorm', 'bc1-rgba-unorm'],
+ ['bc1-rgba-unorm', 'bgra8unorm'],
+ ])
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { viewFormats, enable_required_feature } = t.params;
+
+ if (enable_required_feature) {
+ t.selectDeviceForTextureFormatOrSkipTestCase(viewFormats as GPUTextureFormat[]);
+ }
+ })
+ .fn(async t => {
+ const { viewFormats, canvasType, enable_required_feature } = t.params;
+
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ const canvasConf = {
+ device: t.device,
+ format: 'bgra8unorm' as const,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ viewFormats: viewFormats as GPUTextureFormat[],
+ };
+
+ if (enable_required_feature) {
+ t.expectValidationError(() => {
+ ctx.configure(canvasConf);
+ });
+ } else {
+ t.shouldThrow('TypeError', () => {
+ ctx.configure(canvasConf);
+ });
+ }
+ });
+
+g.test('storage_texture_binding_layout')
+ .desc(
+ `
+ Test creating a GPUStorageTextureBindingLayout with an optional texture format will fail if the
+ required optional feature are not enabled.
+
+ Note: This test has no cases if there are no optional texture formats supporting storage.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .filter(t => kTextureFormatInfo[t.format].storage)
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: {
+ format,
+ },
+ },
+ ],
+ });
+ });
+ });
+
+g.test('color_target_state')
+ .desc(
+ `
+ Test creating a render pipeline with an optional texture format set in GPUColorTargetState will
+ fail if the required optional feature is not enabled.
+
+ Note: This test has no cases if there are no optional texture formats supporting color rendering.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .filter(t => kTextureFormatInfo[t.format].renderable && kTextureFormatInfo[t.format].color)
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ });
+ });
+ });
+
+g.test('depth_stencil_state')
+ .desc(
+ `
+ Test creating a render pipeline with an optional texture format set in GPUColorTargetState will
+ fail if the required optional feature is not enabled.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .filter(
+ t =>
+ kTextureFormatInfo[t.format].renderable &&
+ (kTextureFormatInfo[t.format].depth || kTextureFormatInfo[t.format].stencil)
+ )
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ depthStencil: {
+ format,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
+ });
+
+g.test('render_bundle_encoder_descriptor_color_format')
+ .desc(
+ `
+ Test creating a render bundle encoder with an optional texture format set as one of the color
+ format will fail if the required optional feature is not enabled.
+
+ Note: This test has no cases if there are no optional texture formats supporting color rendering.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .filter(t => kTextureFormatInfo[t.format].renderable && kTextureFormatInfo[t.format].color)
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: [format],
+ });
+ });
+ });
+
+g.test('render_bundle_encoder_descriptor_depth_stencil_format')
+ .desc(
+ `
+ Test creating a render bundle encoder with an optional texture format set as the depth stencil
+ format will fail if the required optional feature is not enabled.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kOptionalTextureFormats)
+ .filter(
+ t =>
+ kTextureFormatInfo[t.format].renderable &&
+ (kTextureFormatInfo[t.format].depth || kTextureFormatInfo[t.format].stencil)
+ )
+ .combine('enable_required_feature', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format, enable_required_feature } = t.params;
+
+ const formatInfo = kTextureFormatInfo[format];
+ if (enable_required_feature) {
+ t.selectDeviceOrSkipTestCase(formatInfo.feature);
+ }
+ })
+ .fn(async t => {
+ const { format, enable_required_feature } = t.params;
+
+ t.shouldThrow(enable_required_feature ? false : 'TypeError', () => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ depthStencilFormat: format,
+ });
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/README.txt
new file mode 100644
index 0000000000..3f2434d4ed
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/capability_checks/limits/README.txt
@@ -0,0 +1,8 @@
+Test everything that shouldn't be valid without a higher-than-specified limit.
+
+- x= that limit {default, max supported (if different), lower than default (TODO: if allowed)}
+
+One file for each limit name.
+
+TODO: implement
+TODO: Also test that "alignment" limits require a power of 2.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts
new file mode 100644
index 0000000000..cfe6ca0749
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/compute_pipeline.spec.ts
@@ -0,0 +1,669 @@
+export const description = `
+createComputePipeline and createComputePipelineAsync validation tests.
+
+Note: entry point matching tests are in shader_module/entry_point.spec.ts
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { kValue } from '../../util/constants.js';
+import { TShaderStage, getShaderWithEntryPoint } from '../../util/shader.js';
+
+import { ValidationTest } from './validation_test.js';
+
+class F extends ValidationTest {
+ getShaderModule(
+ shaderStage: TShaderStage = 'compute',
+ entryPoint: string = 'main'
+ ): GPUShaderModule {
+ return this.device.createShaderModule({
+ code: getShaderWithEntryPoint(shaderStage, entryPoint),
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('basic')
+ .desc(
+ `
+Control case for createComputePipeline and createComputePipelineAsync.
+Call the API with valid compute shader and matching valid entryPoint, making sure that the test function working well.
+`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const { isAsync } = t.params;
+ t.doCreateComputePipelineTest(isAsync, true, {
+ layout: 'auto',
+ compute: { module: t.getShaderModule('compute', 'main'), entryPoint: 'main' },
+ });
+ });
+
+g.test('shader_module,invalid')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) with a invalid compute shader, and check that the APIs catch this error.
+`
+ )
+ .params(u => u.combine('isAsync', [true, false]))
+ .fn(async t => {
+ const { isAsync } = t.params;
+ t.doCreateComputePipelineTest(isAsync, false, {
+ layout: 'auto',
+ compute: {
+ module: t.createInvalidShaderModule(),
+ entryPoint: 'main',
+ },
+ });
+ });
+
+g.test('shader_module,compute')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) with valid but different stage shader and matching entryPoint,
+and check that the APIs only accept compute shader.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('shaderModuleStage', ['compute', 'vertex', 'fragment'] as TShaderStage[])
+ )
+ .fn(async t => {
+ const { isAsync, shaderModuleStage } = t.params;
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.getShaderModule(shaderModuleStage, 'main'),
+ entryPoint: 'main',
+ },
+ };
+ t.doCreateComputePipelineTest(isAsync, shaderModuleStage === 'compute', descriptor);
+ });
+
+g.test('shader_module,device_mismatch')
+ .desc(
+ 'Tests createComputePipeline(Async) cannot be called with a shader module created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('isAsync', [true, false]).combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { isAsync, mismatched } = t.params;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const module = sourceDevice.createShaderModule({
+ code: '@compute @workgroup_size(1) fn main() {}',
+ });
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, !mismatched, descriptor);
+ });
+
+g.test('pipeline_layout,device_mismatch')
+ .desc(
+ 'Tests createComputePipeline(Async) cannot be called with a pipeline layout created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('isAsync', [true, false]).combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { isAsync, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const layout = sourceDevice.createPipelineLayout({ bindGroupLayouts: [] });
+
+ const descriptor = {
+ layout,
+ compute: {
+ module: t.getShaderModule('compute', 'main'),
+ entryPoint: 'main',
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, !mismatched, descriptor);
+ });
+
+g.test('limits,workgroup_storage_size')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for compute using <= device.limits.maxComputeWorkgroupStorageSize bytes of workgroup storage.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { type: 'vec4<f32>', _typeSize: 16 },
+ { type: 'mat4x4<f32>', _typeSize: 64 },
+ ])
+ .beginSubcases()
+ .combine('countDeltaFromLimit', [0, 1])
+ )
+ .fn(async t => {
+ const { isAsync, type, _typeSize, countDeltaFromLimit } = t.params;
+ const countAtLimit = Math.floor(t.device.limits.maxComputeWorkgroupStorageSize / _typeSize);
+ const count = countAtLimit + countDeltaFromLimit;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ var<workgroup> data: array<${type}, ${count}>;
+ @compute @workgroup_size(64) fn main () {
+ _ = data;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ };
+ t.doCreateComputePipelineTest(isAsync, count <= countAtLimit, descriptor);
+ });
+
+g.test('limits,invocations_per_workgroup')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for compute using <= device.limits.maxComputeInvocationsPerWorkgroup per workgroup.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('size', [
+ // Assume maxComputeWorkgroupSizeX/Y >= 129, maxComputeWorkgroupSizeZ >= 33
+ [128, 1, 2],
+ [129, 1, 2],
+ [2, 128, 1],
+ [2, 129, 1],
+ [1, 8, 32],
+ [1, 8, 33],
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, size } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ @compute @workgroup_size(${size.join(',')}) fn main () {
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ };
+
+ t.doCreateComputePipelineTest(
+ isAsync,
+ size[0] * size[1] * size[2] <= t.device.limits.maxComputeInvocationsPerWorkgroup,
+ descriptor
+ );
+ });
+
+g.test('limits,invocations_per_workgroup,each_component')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for compute workgroup_size attribute has each component no more than their limits.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('size', [
+ // Assume maxComputeInvocationsPerWorkgroup >= 256
+ [64],
+ [256, 1, 1],
+ [257, 1, 1],
+ [1, 256, 1],
+ [1, 257, 1],
+ [1, 1, 63],
+ [1, 1, 64],
+ [1, 1, 65],
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, size } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ @compute @workgroup_size(${size.join(',')}) fn main () {
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ };
+
+ size[1] = size[1] ?? 1;
+ size[2] = size[2] ?? 1;
+
+ const _success =
+ size[0] <= t.device.limits.maxComputeWorkgroupSizeX &&
+ size[1] <= t.device.limits.maxComputeWorkgroupSizeY &&
+ size[2] <= t.device.limits.maxComputeWorkgroupSizeZ;
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('overrides,identifier')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for overridable constants identifiers.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { constants: {}, _success: true },
+ { constants: { c0: 0 }, _success: true },
+ { constants: { c0: 0, c1: 1 }, _success: true },
+ { constants: { c9: 0 }, _success: false },
+ { constants: { 1: 0 }, _success: true },
+ { constants: { c3: 0 }, _success: false }, // pipeline constant id is specified for c3
+ { constants: { 2: 0 }, _success: false },
+ { constants: { 1000: 0 }, _success: true },
+ { constants: { 9999: 0 }, _success: false },
+ { constants: { 1000: 0, c2: 0 }, _success: false },
+ ] as { constants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ override c0: bool = true; // type: bool
+ override c1: u32 = 0u; // default override
+ @id(1000) override c2: u32 = 10u; // default
+ @id(1) override c3: u32 = 11u; // default
+ @compute @workgroup_size(1) fn main () {
+ // make sure the overridable constants are not optimized out
+ _ = u32(c0);
+ _ = u32(c1);
+ _ = u32(c2);
+ _ = u32(c3);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('overrides,uninitialized')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for uninitialized overridable constants.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { constants: {}, _success: false },
+ { constants: { c0: 0, c2: 0, c8: 0 }, _success: false }, // c5 is missing
+ { constants: { c0: 0, c2: 0, c5: 0, c8: 0 }, _success: true },
+ { constants: { c0: 0, c2: 0, c5: 0, c8: 0, c1: 0 }, _success: true },
+ ] as { constants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ override c0: bool; // type: bool
+ override c1: bool = false; // default override
+ override c2: f32; // type: float32
+ override c3: f32 = 0.0; // default override
+ override c4: f32 = 4.0; // default
+ override c5: i32; // type: int32
+ override c6: i32 = 0; // default override
+ override c7: i32 = 7; // default
+ override c8: u32; // type: uint32
+ override c9: u32 = 0u; // default override
+ @id(1000) override c10: u32 = 10u; // default
+ @compute @workgroup_size(1) fn main () {
+ // make sure the overridable constants are not optimized out
+ _ = u32(c0);
+ _ = u32(c1);
+ _ = u32(c2);
+ _ = u32(c3);
+ _ = u32(c4);
+ _ = u32(c5);
+ _ = u32(c6);
+ _ = u32(c7);
+ _ = u32(c8);
+ _ = u32(c9);
+ _ = u32(c10);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('overrides,value,type_error')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for constant values like inf, NaN will results in TypeError.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { constants: { cf: 1 }, _success: true }, // control
+ { constants: { cf: NaN }, _success: false },
+ { constants: { cf: Number.POSITIVE_INFINITY }, _success: false },
+ { constants: { cf: Number.NEGATIVE_INFINITY }, _success: false },
+ ] as const)
+ )
+ .fn(async t => {
+ const { isAsync, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ override cf: f32 = 0.0;
+ @compute @workgroup_size(1) fn main () {
+ _ = cf;
+ }`,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor, 'TypeError');
+ });
+
+g.test('overrides,value,validation_error')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for unrepresentable constant values in compute stage.
+
+TODO(#2060): test with last_f64_castable.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { constants: { cu: kValue.u32.min }, _success: true },
+ { constants: { cu: kValue.u32.min - 1 }, _success: false },
+ { constants: { cu: kValue.u32.max }, _success: true },
+ { constants: { cu: kValue.u32.max + 1 }, _success: false },
+ { constants: { ci: kValue.i32.negative.min }, _success: true },
+ { constants: { ci: kValue.i32.negative.min - 1 }, _success: false },
+ { constants: { ci: kValue.i32.positive.max }, _success: true },
+ { constants: { ci: kValue.i32.positive.max + 1 }, _success: false },
+ { constants: { cf: kValue.f32.negative.min }, _success: true },
+ { constants: { cf: kValue.f32.negative.first_f64_not_castable }, _success: false },
+ { constants: { cf: kValue.f32.positive.max }, _success: true },
+ { constants: { cf: kValue.f32.positive.first_f64_not_castable }, _success: false },
+ // Conversion to boolean can't fail
+ { constants: { cb: Number.MAX_VALUE }, _success: true },
+ { constants: { cb: kValue.i32.negative.min - 1 }, _success: true },
+ ] as { constants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ override cb: bool = false;
+ override cu: u32 = 0u;
+ override ci: i32 = 0;
+ override cf: f32 = 0.0;
+ @compute @workgroup_size(1) fn main () {
+ _ = cb;
+ _ = cu;
+ _ = ci;
+ _ = cf;
+ }`,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('overrides,value,validation_error,f16')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for unrepresentable f16 constant values in compute stage.
+
+TODO(#2060): Tighten the cases around the valid/invalid boundary once we have WGSL spec
+clarity on whether values like f16.positive.last_f64_castable would be valid. See issue.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { constants: { cf16: kValue.f16.negative.min }, _success: true },
+ { constants: { cf16: kValue.f16.negative.first_f64_not_castable }, _success: false },
+ { constants: { cf16: kValue.f16.positive.max }, _success: true },
+ { constants: { cf16: kValue.f16.positive.first_f64_not_castable }, _success: false },
+ { constants: { cf16: kValue.f32.negative.min }, _success: false },
+ { constants: { cf16: kValue.f32.positive.max }, _success: false },
+ { constants: { cf16: kValue.f32.negative.first_f64_not_castable }, _success: false },
+ { constants: { cf16: kValue.f32.positive.first_f64_not_castable }, _success: false },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] });
+ })
+ .fn(async t => {
+ const { isAsync, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ enable f16;
+
+ override cf16: f16 = 0.0h;
+ @compute @workgroup_size(1) fn main () {
+ _ = cf16;
+ }`,
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+const kOverridesWorkgroupSizeShaders = {
+ u32: `
+override x: u32 = 1u;
+override y: u32 = 1u;
+override z: u32 = 1u;
+@compute @workgroup_size(x, y, z) fn main () {
+ _ = 0u;
+}
+`,
+ i32: `
+override x: i32 = 1;
+override y: i32 = 1;
+override z: i32 = 1;
+@compute @workgroup_size(x, y, z) fn main () {
+ _ = 0u;
+}
+`,
+};
+
+g.test('overrides,workgroup_size')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for overridable constants used for workgroup size.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('type', ['u32', 'i32'] as const)
+ .combineWithParams([
+ { constants: {}, _success: true },
+ { constants: { x: 0, y: 0, z: 0 }, _success: false },
+ { constants: { x: 1, y: -1, z: 1 }, _success: false },
+ { constants: { x: 1, y: 0, z: 0 }, _success: false },
+ { constants: { x: 16, y: 1, z: 1 }, _success: true },
+ ] as { constants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, type, constants, _success } = t.params;
+
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: kOverridesWorkgroupSizeShaders[type],
+ }),
+ entryPoint: 'main',
+ constants,
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('overrides,workgroup_size,limits')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for overridable constants for workgroupSize exceeds device limits.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combine('type', ['u32', 'i32'] as const)
+ )
+ .fn(async t => {
+ const { isAsync, type } = t.params;
+
+ const limits = t.device.limits;
+
+ const testFn = (x: number, y: number, z: number, _success: boolean) => {
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: kOverridesWorkgroupSizeShaders[type],
+ }),
+ entryPoint: 'main',
+ constants: {
+ x,
+ y,
+ z,
+ },
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ };
+
+ testFn(limits.maxComputeWorkgroupSizeX, 1, 1, true);
+ testFn(limits.maxComputeWorkgroupSizeX + 1, 1, 1, false);
+ testFn(1, limits.maxComputeWorkgroupSizeY, 1, true);
+ testFn(1, limits.maxComputeWorkgroupSizeY + 1, 1, false);
+ testFn(1, 1, limits.maxComputeWorkgroupSizeZ, true);
+ testFn(1, 1, limits.maxComputeWorkgroupSizeZ + 1, false);
+ testFn(
+ limits.maxComputeWorkgroupSizeX,
+ limits.maxComputeWorkgroupSizeY,
+ limits.maxComputeWorkgroupSizeZ,
+ limits.maxComputeWorkgroupSizeX *
+ limits.maxComputeWorkgroupSizeY *
+ limits.maxComputeWorkgroupSizeZ <=
+ limits.maxComputeInvocationsPerWorkgroup
+ );
+ });
+
+g.test('overrides,workgroup_size,limits,workgroup_storage_size')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) validation for overridable constants for workgroupStorageSize exceeds device limits.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ )
+ .fn(async t => {
+ const { isAsync } = t.params;
+
+ const limits = t.device.limits;
+
+ const kVec4Size = 16;
+ const maxVec4Count = limits.maxComputeWorkgroupStorageSize / kVec4Size;
+ const kMat4Size = 64;
+ const maxMat4Count = limits.maxComputeWorkgroupStorageSize / kMat4Size;
+
+ const testFn = (vec4Count: number, mat4Count: number, _success: boolean) => {
+ const descriptor = {
+ layout: 'auto' as const,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ override a: u32;
+ override b: u32;
+ ${vec4Count <= 0 ? '' : 'var<workgroup> vec4_data: array<vec4<f32>, a>;'}
+ ${mat4Count <= 0 ? '' : 'var<workgroup> mat4_data: array<mat4x4<f32>, b>;'}
+ @compute @workgroup_size(1) fn main() {
+ ${vec4Count <= 0 ? '' : '_ = vec4_data[0];'}
+ ${mat4Count <= 0 ? '' : '_ = mat4_data[0];'}
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: {
+ a: vec4Count,
+ b: mat4Count,
+ },
+ },
+ };
+
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ };
+
+ testFn(1, 1, true);
+ testFn(maxVec4Count + 1, 0, false);
+ testFn(0, maxMat4Count + 1, false);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts
new file mode 100644
index 0000000000..2815cddcc6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts
@@ -0,0 +1,1131 @@
+export const description = `
+ createBindGroup validation tests.
+
+ TODO: Ensure sure tests cover all createBindGroup validation rules.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../common/util/util.js';
+import {
+ allBindingEntries,
+ bindingTypeInfo,
+ bufferBindingEntries,
+ bufferBindingTypeInfo,
+ kAllTextureFormats,
+ kBindableResources,
+ kBufferBindingTypes,
+ kBufferUsages,
+ kCompareFunctions,
+ kLimitInfo,
+ kSamplerBindingTypes,
+ kTextureFormatInfo,
+ kTextureUsages,
+ kTextureViewDimensions,
+ sampledAndStorageBindingEntries,
+ texBindingTypeInfo,
+} from '../../capability_info.js';
+import { GPUConst } from '../../constants.js';
+import { kResourceStates } from '../../gpu_test.js';
+import { getTextureDimensionFromView } from '../../util/texture/base.js';
+
+import { ValidationTest } from './validation_test.js';
+
+function clone<T extends GPUTextureDescriptor>(descriptor: T): T {
+ return JSON.parse(JSON.stringify(descriptor));
+}
+
+export const g = makeTestGroup(ValidationTest);
+
+const kStorageTextureFormats = kAllTextureFormats.filter(f => kTextureFormatInfo[f].storage);
+
+g.test('binding_count_mismatch')
+ .desc('Test that the number of entries must match the number of entries in the BindGroupLayout.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('layoutEntryCount', [1, 2, 3])
+ .combine('bindGroupEntryCount', [1, 2, 3])
+ )
+ .fn(async t => {
+ const { layoutEntryCount, bindGroupEntryCount } = t.params;
+
+ const layoutEntries: Array<GPUBindGroupLayoutEntry> = [];
+ for (let i = 0; i < layoutEntryCount; ++i) {
+ layoutEntries.push({
+ binding: i,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' },
+ });
+ }
+ const bindGroupLayout = t.device.createBindGroupLayout({ entries: layoutEntries });
+
+ const entries: Array<GPUBindGroupEntry> = [];
+ for (let i = 0; i < bindGroupEntryCount; ++i) {
+ entries.push({
+ binding: i,
+ resource: { buffer: t.getStorageBuffer() },
+ });
+ }
+
+ const shouldError = layoutEntryCount !== bindGroupEntryCount;
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries,
+ layout: bindGroupLayout,
+ });
+ }, shouldError);
+ });
+
+g.test('binding_must_be_present_in_layout')
+ .desc(
+ 'Test that the binding slot for each entry matches a binding slot defined in the BindGroupLayout.'
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('layoutBinding', [0, 1, 2])
+ .combine('binding', [0, 1, 2])
+ )
+ .fn(async t => {
+ const { layoutBinding, binding } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ { binding: layoutBinding, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ ],
+ });
+
+ const descriptor = {
+ entries: [{ binding, resource: { buffer: t.getStorageBuffer() } }],
+ layout: bindGroupLayout,
+ };
+
+ const shouldError = layoutBinding !== binding;
+ t.expectValidationError(() => {
+ t.device.createBindGroup(descriptor);
+ }, shouldError);
+ });
+
+g.test('binding_must_contain_resource_defined_in_layout')
+ .desc(
+ 'Test that only compatible resource types specified in the BindGroupLayout are allowed for each entry.'
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('resourceType', kBindableResources)
+ .combine('entry', allBindingEntries(false))
+ )
+ .fn(t => {
+ const { resourceType, entry } = t.params;
+ const info = bindingTypeInfo(entry);
+
+ const layout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility: GPUShaderStage.COMPUTE, ...entry }],
+ });
+
+ const resource = t.getBindingResource(resourceType);
+
+ let resourceBindingIsCompatible;
+ switch (info.resource) {
+ // Either type of sampler may be bound to a filtering sampler binding.
+ case 'filtSamp':
+ resourceBindingIsCompatible = resourceType === 'filtSamp' || resourceType === 'nonFiltSamp';
+ break;
+ // But only non-filtering samplers can be used with non-filtering sampler bindings.
+ case 'nonFiltSamp':
+ resourceBindingIsCompatible = resourceType === 'nonFiltSamp';
+ break;
+ default:
+ resourceBindingIsCompatible = info.resource === resourceType;
+ break;
+ }
+ t.expectValidationError(() => {
+ t.device.createBindGroup({ layout, entries: [{ binding: 0, resource }] });
+ }, !resourceBindingIsCompatible);
+ });
+
+g.test('texture_binding_must_have_correct_usage')
+ .desc('Tests that texture bindings must have the correct usage.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('entry', sampledAndStorageBindingEntries(false))
+ .combine('usage', kTextureUsages)
+ .unless(({ entry, usage }) => {
+ const info = texBindingTypeInfo(entry);
+ // Can't create the texture for this (usage=STORAGE_BINDING and sampleCount=4), so skip.
+ return usage === GPUConst.TextureUsage.STORAGE_BINDING && info.resource === 'sampledTexMS';
+ })
+ )
+ .fn(async t => {
+ const { entry, usage } = t.params;
+ const info = texBindingTypeInfo(entry);
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, ...entry }],
+ });
+
+ // The `RENDER_ATTACHMENT` usage must be specified if sampleCount > 1 according to WebGPU SPEC.
+ const appliedUsage =
+ info.resource === 'sampledTexMS' ? usage | GPUConst.TextureUsage.RENDER_ATTACHMENT : usage;
+
+ const descriptor = {
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm' as const,
+ usage: appliedUsage,
+ sampleCount: info.resource === 'sampledTexMS' ? 4 : 1,
+ };
+ const resource = t.device.createTexture(descriptor).createView();
+
+ const shouldError = (usage & info.usage) === 0;
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource }],
+ layout: bindGroupLayout,
+ });
+ }, shouldError);
+ });
+
+g.test('texture_must_have_correct_component_type')
+ .desc(
+ `
+ Tests that texture bindings must have a format that matches the sample type specified in the BindGroupLayout.
+ - Tests a compatible format for every sample type
+ - Tests an incompatible format for every sample type`
+ )
+ .params(u => u.combine('sampleType', ['float', 'sint', 'uint'] as const))
+ .fn(async t => {
+ const { sampleType } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ texture: { sampleType },
+ },
+ ],
+ });
+
+ let format: GPUTextureFormat;
+ if (sampleType === 'float') {
+ format = 'r8unorm';
+ } else if (sampleType === 'sint') {
+ format = 'r8sint';
+ } else if (sampleType === 'uint') {
+ format = 'r8uint';
+ } else {
+ unreachable('Unexpected texture component type');
+ }
+
+ const goodDescriptor = {
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ // Control case
+ t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: t.device.createTexture(goodDescriptor).createView(),
+ },
+ ],
+ layout: bindGroupLayout,
+ });
+
+ function* mismatchedTextureFormats(): Iterable<GPUTextureFormat> {
+ if (sampleType !== 'float') {
+ yield 'r8unorm';
+ }
+ if (sampleType !== 'sint') {
+ yield 'r8sint';
+ }
+ if (sampleType !== 'uint') {
+ yield 'r8uint';
+ }
+ }
+
+ // Mismatched texture binding formats are not valid.
+ for (const mismatchedTextureFormat of mismatchedTextureFormats()) {
+ const badDescriptor: GPUTextureDescriptor = clone(goodDescriptor);
+ badDescriptor.format = mismatchedTextureFormat;
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: t.device.createTexture(badDescriptor).createView() }],
+ layout: bindGroupLayout,
+ });
+ });
+ }
+ });
+
+g.test('texture_must_have_correct_dimension')
+ .desc(
+ `
+ Test that bound texture views match the dimensions supplied in the BindGroupLayout
+ - Test for every GPUTextureViewDimension
+ - Test for both TEXTURE_BINDING and STORAGE_BINDING.
+ `
+ )
+ .params(u =>
+ u
+ .combine('usage', [
+ GPUConst.TextureUsage.TEXTURE_BINDING,
+ GPUConst.TextureUsage.STORAGE_BINDING,
+ ])
+ .combine('viewDimension', kTextureViewDimensions)
+ .unless(
+ p =>
+ p.usage === GPUConst.TextureUsage.STORAGE_BINDING &&
+ (p.viewDimension === 'cube' || p.viewDimension === 'cube-array')
+ )
+ .beginSubcases()
+ .combine('dimension', kTextureViewDimensions)
+ )
+ .fn(async t => {
+ const { usage, viewDimension, dimension } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ usage === GPUTextureUsage.TEXTURE_BINDING
+ ? {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ texture: { viewDimension },
+ }
+ : {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ storageTexture: { access: 'write-only', format: 'rgba8unorm', viewDimension },
+ },
+ ],
+ });
+
+ let height = 16;
+ let depthOrArrayLayers = 6;
+ if (dimension === '1d') {
+ height = 1;
+ depthOrArrayLayers = 1;
+ }
+
+ const texture = t.device.createTexture({
+ size: { width: 16, height, depthOrArrayLayers },
+ format: 'rgba8unorm' as const,
+ usage,
+ dimension: getTextureDimensionFromView(dimension),
+ });
+
+ const shouldError = viewDimension !== dimension;
+ const textureView = texture.createView({ dimension });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: textureView }],
+ layout: bindGroupLayout,
+ });
+ }, shouldError);
+ });
+
+g.test('multisampled_validation')
+ .desc(
+ `
+ Test that the sample count of the texture is greater than 1 if the BindGroup entry's
+ multisampled is true. Otherwise, the texture's sampleCount should be 1.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('multisampled', [true, false])
+ .beginSubcases()
+ .combine('sampleCount', [1, 4])
+ )
+ .fn(async t => {
+ const { multisampled, sampleCount } = t.params;
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ texture: { multisampled },
+ },
+ ],
+ });
+
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm' as const,
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount,
+ });
+
+ const isValid = (!multisampled && sampleCount === 1) || (multisampled && sampleCount > 1);
+
+ const textureView = texture.createView();
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: textureView }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('buffer_offset_and_size_for_bind_groups_match')
+ .desc(
+ `
+ Test that a buffer binding's [offset, offset + size) must be contained in the BindGroup entry's buffer.
+ - Test for various offsets and sizes`
+ )
+ .paramsSubcasesOnly([
+ { offset: 0, size: 512, _success: true }, // offset 0 is valid
+ { offset: 256, size: 256, _success: true }, // offset 256 (aligned) is valid
+
+ // Touching the end of the buffer
+ { offset: 0, size: 1024, _success: true },
+ { offset: 0, size: undefined, _success: true },
+ { offset: 256 * 3, size: 256, _success: true },
+ { offset: 256 * 3, size: undefined, _success: true },
+
+ // Zero-sized bindings
+ { offset: 0, size: 0, _success: false },
+ { offset: 256, size: 0, _success: false },
+ { offset: 1024, size: 0, _success: false },
+ { offset: 1024, size: undefined, _success: false },
+
+ // Unaligned buffer offset is invalid
+ { offset: 1, size: 256, _success: false },
+ { offset: 1, size: undefined, _success: false },
+ { offset: 128, size: 256, _success: false },
+ { offset: 255, size: 256, _success: false },
+
+ // Out-of-bounds
+ { offset: 256 * 5, size: 0, _success: false }, // offset is OOB
+ { offset: 0, size: 256 * 5, _success: false }, // size is OOB
+ { offset: 1024, size: 1, _success: false }, // offset+size is OOB
+ ])
+ .fn(async t => {
+ const { offset, size, _success } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }],
+ });
+
+ const buffer = t.device.createBuffer({
+ size: 1024,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ const descriptor = {
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer, offset, size },
+ },
+ ],
+ layout: bindGroupLayout,
+ };
+
+ if (_success) {
+ // Control case
+ t.device.createBindGroup(descriptor);
+ } else {
+ // Buffer offset and/or size don't match in bind groups.
+ t.expectValidationError(() => {
+ t.device.createBindGroup(descriptor);
+ });
+ }
+ });
+
+g.test('minBindingSize')
+ .desc('Tests that minBindingSize is correctly enforced.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('minBindingSize', [undefined, 4, 8, 256])
+ .expand('size', ({ minBindingSize }) =>
+ minBindingSize !== undefined
+ ? [minBindingSize - 4, minBindingSize, minBindingSize + 4]
+ : [4, 256]
+ )
+ )
+ .fn(t => {
+ const { size, minBindingSize } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'storage',
+ minBindingSize,
+ },
+ },
+ ],
+ });
+
+ const storageBuffer = t.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: storageBuffer,
+ },
+ },
+ ],
+ });
+ }, minBindingSize !== undefined && size < minBindingSize);
+ });
+
+g.test('buffer,resource_state')
+ .desc('Test bind group creation with various buffer resource states')
+ .paramsSubcasesOnly(u =>
+ u.combine('state', kResourceStates).combine('entry', bufferBindingEntries(true))
+ )
+ .fn(t => {
+ const { state, entry } = t.params;
+
+ assert(entry.buffer !== undefined);
+ const info = bufferBindingTypeInfo(entry.buffer);
+
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ {
+ ...entry,
+ binding: 0,
+ visibility: info.validStages,
+ },
+ ],
+ });
+
+ const buffer = t.createBufferWithState(state, {
+ usage: info.usage,
+ size: 4,
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ ],
+ });
+ }, state === 'invalid');
+ });
+
+g.test('texture,resource_state')
+ .desc('Test bind group creation with various texture resource states')
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('state', kResourceStates)
+ .combine('entry', sampledAndStorageBindingEntries(true, 'rgba8unorm'))
+ )
+ .fn(t => {
+ const { state, entry } = t.params;
+ const info = texBindingTypeInfo(entry);
+
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ {
+ ...entry,
+ binding: 0,
+ visibility: info.validStages,
+ },
+ ],
+ });
+
+ // The `RENDER_ATTACHMENT` usage must be specified if sampleCount > 1 according to WebGPU SPEC.
+ const usage = entry.texture?.multisampled
+ ? info.usage | GPUConst.TextureUsage.RENDER_ATTACHMENT
+ : info.usage;
+ const texture = t.createTextureWithState(state, {
+ usage,
+ size: [1, 1],
+ format: 'rgba8unorm',
+ sampleCount: entry.texture?.multisampled ? 4 : 1,
+ });
+
+ let textureView: GPUTextureView;
+ t.expectValidationError(() => {
+ textureView = texture.createView();
+ }, state === 'invalid');
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: textureView,
+ },
+ ],
+ });
+ }, state === 'invalid');
+ });
+
+g.test('bind_group_layout,device_mismatch')
+ .desc(
+ 'Tests createBindGroup cannot be called with a bind group layout created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const mismatched = t.params.mismatched;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const bgl = sourceDevice.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUConst.ShaderStage.VERTEX,
+ buffer: {},
+ },
+ ],
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer: t.getUniformBuffer() },
+ },
+ ],
+ });
+ }, mismatched);
+ });
+
+g.test('binding_resources,device_mismatch')
+ .desc(
+ `
+ Tests createBindGroup cannot be called with various resources created from another device
+ Test with two resources to make sure all resources can be validated:
+ - resource0 and resource1 from same device
+ - resource0 and resource1 from different device
+
+ TODO: test GPUExternalTexture as a resource
+ `
+ )
+ .params(u =>
+ u
+ .combine('entry', [
+ { buffer: { type: 'storage' } },
+ { sampler: { type: 'filtering' } },
+ { texture: { multisampled: false } },
+ { storageTexture: { access: 'write-only', format: 'rgba8unorm' } },
+ ] as const)
+ .beginSubcases()
+ .combineWithParams([
+ { resource0Mismatched: false, resource1Mismatched: false }, //control case
+ { resource0Mismatched: true, resource1Mismatched: false },
+ { resource0Mismatched: false, resource1Mismatched: true },
+ ])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { entry, resource0Mismatched, resource1Mismatched } = t.params;
+
+ const info = bindingTypeInfo(entry);
+
+ const resource0 = resource0Mismatched
+ ? t.getDeviceMismatchedBindingResource(info.resource)
+ : t.getBindingResource(info.resource);
+ const resource1 = resource1Mismatched
+ ? t.getDeviceMismatchedBindingResource(info.resource)
+ : t.getBindingResource(info.resource);
+
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: info.validStages,
+ ...entry,
+ },
+ {
+ binding: 1,
+ visibility: info.validStages,
+ ...entry,
+ },
+ ],
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: resource0,
+ },
+ {
+ binding: 1,
+ resource: resource1,
+ },
+ ],
+ });
+ }, resource0Mismatched || resource1Mismatched);
+ });
+
+g.test('storage_texture,usage')
+ .desc(
+ `
+ Test that the texture usage contains STORAGE_BINDING if the BindGroup entry defines
+ storageTexture.
+ `
+ )
+ .params(u =>
+ u //
+ // If usage0 and usage1 are the same, the usage being test is a single usage. Otherwise, it's
+ // a combined usage.
+ .combine('usage0', kTextureUsages)
+ .combine('usage1', kTextureUsages)
+ )
+ .fn(async t => {
+ const { usage0, usage1 } = t.params;
+
+ const usage = usage0 | usage1;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ storageTexture: { access: 'write-only', format: 'rgba8unorm' },
+ },
+ ],
+ });
+
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm' as const,
+ usage,
+ });
+
+ const isValid = GPUTextureUsage.STORAGE_BINDING & usage;
+
+ const textureView = texture.createView();
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: textureView }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('storage_texture,mip_level_count')
+ .desc(
+ `
+ Test that the mip level count of the resource of the BindGroup entry as a descriptor is 1 if the
+ BindGroup entry defines storageTexture. If the mip level count is not 1, a validation error
+ should be generated.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('baseMipLevel', [1, 2])
+ .combine('mipLevelCount', [1, 2])
+ )
+ .fn(async t => {
+ const { baseMipLevel, mipLevelCount } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ storageTexture: { access: 'write-only', format: 'rgba8unorm' },
+ },
+ ],
+ });
+
+ const MIP_LEVEL_COUNT = 4;
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm' as const,
+ usage: GPUTextureUsage.STORAGE_BINDING,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ });
+
+ const textureView = texture.createView({ baseMipLevel, mipLevelCount });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: textureView }],
+ layout: bindGroupLayout,
+ });
+ }, mipLevelCount !== 1);
+ });
+
+g.test('storage_texture,format')
+ .desc(
+ `
+ Test that the format of the storage texture is equal to resource's descriptor format if the
+ BindGroup entry defines storageTexture.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('storageTextureFormat', kStorageTextureFormats)
+ .combine('resourceFormat', kStorageTextureFormats)
+ )
+ .fn(async t => {
+ const { storageTextureFormat, resourceFormat } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ storageTexture: { access: 'write-only', format: storageTextureFormat },
+ },
+ ],
+ });
+
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: resourceFormat,
+ usage: GPUTextureUsage.STORAGE_BINDING,
+ });
+
+ const isValid = storageTextureFormat === resourceFormat;
+ const textureView = texture.createView({ format: resourceFormat });
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: textureView }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('buffer,usage')
+ .desc(
+ `
+ Test that the buffer usage contains 'UNIFORM' if the BindGroup entry defines buffer and it's
+ type is 'uniform', and the buffer usage contains 'STORAGE' if the BindGroup entry's buffer type
+ is 'storage'|read-only-storage'.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('type', kBufferBindingTypes)
+ // If usage0 and usage1 are the same, the usage being test is a single usage. Otherwise, it's
+ // a combined usage.
+ .beginSubcases()
+ .combine('usage0', kBufferUsages)
+ .combine('usage1', kBufferUsages)
+ .unless(
+ ({ usage0, usage1 }) =>
+ ((usage0 | usage1) & (GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE)) !==
+ 0
+ )
+ )
+ .fn(async t => {
+ const { type, usage0, usage1 } = t.params;
+
+ const usage = usage0 | usage1;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type },
+ },
+ ],
+ });
+
+ const buffer = t.device.createBuffer({
+ size: 4,
+ usage,
+ });
+
+ let isValid = false;
+ if (type === 'uniform') {
+ isValid = GPUBufferUsage.UNIFORM & usage ? true : false;
+ } else if (type === 'storage' || type === 'read-only-storage') {
+ isValid = GPUBufferUsage.STORAGE & usage ? true : false;
+ }
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer } }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('buffer,resource_offset')
+ .desc(
+ `
+ Test that the resource.offset of the BindGroup entry is a multiple of limits.
+ 'minUniformBufferOffsetAlignment|minStorageBufferOffsetAlignment' if the BindGroup entry defines
+ buffer and the buffer type is 'uniform|storage|read-only-storage'.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('type', kBufferBindingTypes)
+ .beginSubcases()
+ .expand('offset', ({ type }) =>
+ type === 'uniform'
+ ? [
+ kLimitInfo.minUniformBufferOffsetAlignment.default,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default + 2,
+ ]
+ : [
+ kLimitInfo.minStorageBufferOffsetAlignment.default,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default + 2,
+ ]
+ )
+ )
+ .fn(async t => {
+ const { type, offset } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type },
+ },
+ ],
+ });
+
+ let usage, isValid;
+ if (type === 'uniform') {
+ usage = GPUBufferUsage.UNIFORM;
+ isValid = offset % kLimitInfo.minUniformBufferOffsetAlignment.default === 0;
+ } else {
+ usage = GPUBufferUsage.STORAGE;
+ isValid = offset % kLimitInfo.minStorageBufferOffsetAlignment.default === 0;
+ }
+
+ const buffer = t.device.createBuffer({
+ size: 1024,
+ usage,
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer, offset } }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('buffer,resource_binding_size')
+ .desc(
+ `
+ Test that the buffer binding size of the BindGroup entry is equal to or less than limits.
+ 'maxUniformBufferBindingSize|maxStorageBufferBindingSize' if the BindGroup entry defines
+ buffer and the buffer type is 'uniform|storage|read-only-storage'.
+ `
+ )
+ .params(u =>
+ u
+ .combine('type', kBufferBindingTypes)
+ .beginSubcases()
+ // Test a size of 1 (for uniform buffer) or 4 (for storage and read-only storage buffer)
+ // then values just within and just above the limit.
+ .expand('bindingSize', ({ type }) =>
+ type === 'uniform'
+ ? [
+ 1,
+ kLimitInfo.maxUniformBufferBindingSize.default,
+ kLimitInfo.maxUniformBufferBindingSize.default + 1,
+ ]
+ : [
+ 4,
+ kLimitInfo.maxStorageBufferBindingSize.default,
+ kLimitInfo.maxStorageBufferBindingSize.default + 4,
+ ]
+ )
+ )
+ .fn(async t => {
+ const { type, bindingSize } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type },
+ },
+ ],
+ });
+
+ let usage, isValid;
+ if (type === 'uniform') {
+ usage = GPUBufferUsage.UNIFORM;
+ isValid = bindingSize <= kLimitInfo.maxUniformBufferBindingSize.default;
+ } else {
+ usage = GPUBufferUsage.STORAGE;
+ isValid = bindingSize <= kLimitInfo.maxStorageBufferBindingSize.default;
+ }
+
+ const buffer = t.device.createBuffer({
+ size: kLimitInfo.maxStorageBufferBindingSize.default,
+ usage,
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer, size: bindingSize } }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('buffer,effective_buffer_binding_size')
+ .desc(
+ `
+ Test that the effective buffer binding size of the BindGroup entry must be a multiple of 4 if the
+ buffer type is 'storage|read-only-storage', while there is no such restriction on uniform buffers.
+`
+ )
+ .params(u =>
+ u
+ .combine('type', kBufferBindingTypes)
+ .beginSubcases()
+ .expand('offset', ({ type }) =>
+ type === 'uniform'
+ ? [0, kLimitInfo.minUniformBufferOffsetAlignment.default]
+ : [0, kLimitInfo.minStorageBufferOffsetAlignment.default]
+ )
+ .expand('bufferSize', ({ type }) =>
+ type === 'uniform'
+ ? [
+ kLimitInfo.minUniformBufferOffsetAlignment.default + 8,
+ kLimitInfo.minUniformBufferOffsetAlignment.default + 10,
+ ]
+ : [
+ kLimitInfo.minStorageBufferOffsetAlignment.default + 8,
+ kLimitInfo.minStorageBufferOffsetAlignment.default + 10,
+ ]
+ )
+ .combine('bindingSize', [undefined, 2, 4, 6])
+ )
+ .fn(async t => {
+ const { type, offset, bufferSize, bindingSize } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type },
+ },
+ ],
+ });
+
+ const effectiveBindingSize = bindingSize ?? bufferSize - offset;
+ let usage, isValid;
+ if (type === 'uniform') {
+ usage = GPUBufferUsage.UNIFORM;
+ isValid = true;
+ } else {
+ usage = GPUBufferUsage.STORAGE;
+ isValid = effectiveBindingSize % 4 === 0;
+ }
+
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage,
+ });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer, offset, size: bindingSize } }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
+
+g.test('sampler,device_mismatch')
+ .desc(`Tests createBindGroup cannot be called with a sampler created from another device.`)
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ sampler: { type: 'filtering' as const },
+ },
+ ],
+ });
+
+ const sampler = sourceDevice.createSampler();
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: sampler }],
+ layout: bindGroupLayout,
+ });
+ }, mismatched);
+ });
+
+g.test('sampler,compare_function_with_binding_type')
+ .desc(
+ `
+ Test that the sampler of the BindGroup has a 'compareFunction' value if the sampler type of the
+ BindGroupLayout is 'comparison'. Other sampler types should not have 'compare' field in
+ the descriptor of the sampler.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('bgType', kSamplerBindingTypes)
+ .beginSubcases()
+ .combine('compareFunction', [undefined, ...kCompareFunctions])
+ )
+ .fn(async t => {
+ const { bgType, compareFunction } = t.params;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ sampler: { type: bgType },
+ },
+ ],
+ });
+
+ const isValid =
+ bgType === 'comparison' ? compareFunction !== undefined : compareFunction === undefined;
+
+ const sampler = t.device.createSampler({ compare: compareFunction });
+
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ entries: [{ binding: 0, resource: sampler }],
+ layout: bindGroupLayout,
+ });
+ }, !isValid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts
new file mode 100644
index 0000000000..6971439598
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts
@@ -0,0 +1,456 @@
+export const description = `
+createBindGroupLayout validation tests.
+
+TODO: make sure tests are complete.
+`;
+
+import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import {
+ kAllTextureFormats,
+ kShaderStages,
+ kShaderStageCombinations,
+ kStorageTextureAccessValues,
+ kTextureFormatInfo,
+ kTextureSampleTypes,
+ kTextureViewDimensions,
+ allBindingEntries,
+ bindingTypeInfo,
+ bufferBindingTypeInfo,
+ kBufferBindingTypes,
+ BGLEntry,
+} from '../../capability_info.js';
+
+import { ValidationTest } from './validation_test.js';
+
+function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
+ return JSON.parse(JSON.stringify(descriptor));
+}
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('duplicate_bindings')
+ .desc('Test that uniqueness of binding numbers across entries is enforced.')
+ .paramsSubcasesOnly([
+ { bindings: [0, 1], _valid: true },
+ { bindings: [0, 0], _valid: false },
+ ])
+ .fn(async t => {
+ const { bindings, _valid } = t.params;
+ const entries: Array<GPUBindGroupLayoutEntry> = [];
+
+ for (const binding of bindings) {
+ entries.push({
+ binding,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' as const },
+ });
+ }
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries,
+ });
+ }, !_valid);
+ });
+
+// MAINTENANCE_TODO: Move this into kLimits with the proper name after the spec PR lands.
+// https://github.com/gpuweb/gpuweb/pull/3318
+const kMaxBindingsPerBindGroup = 640;
+
+g.test('maximum_binding_limit')
+ .desc(
+ `
+ Test that a validation error is generated if the binding number exceeds the maximum binding limit.
+
+ TODO: Need to also test with higher limits enabled on the device, once we have a way to do that.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('binding', [1, 4, 8, 256, kMaxBindingsPerBindGroup - 1, kMaxBindingsPerBindGroup])
+ )
+ .fn(async t => {
+ const { binding } = t.params;
+ const entries: Array<GPUBindGroupLayoutEntry> = [];
+
+ entries.push({
+ binding,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' as const },
+ });
+
+ const success = binding < kMaxBindingsPerBindGroup;
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries,
+ });
+ }, !success);
+ });
+
+g.test('visibility')
+ .desc(
+ `
+ Test that only the appropriate combinations of visibilities are allowed for each resource type.
+ - Test each possible combination of shader stage visibilities.
+ - Test each type of bind group resource.`
+ )
+ .params(u =>
+ u
+ .combine('visibility', kShaderStageCombinations)
+ .beginSubcases()
+ .combine('entry', allBindingEntries(false))
+ )
+ .fn(async t => {
+ const { visibility, entry } = t.params;
+ const info = bindingTypeInfo(entry);
+
+ const success = (visibility & ~info.validStages) === 0;
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility, ...entry }],
+ });
+ }, !success);
+ });
+
+g.test('visibility,VERTEX_shader_stage_buffer_type')
+ .desc(
+ `
+ Test that a validation error is generated if the buffer type is 'storage' when the
+ visibility of the entry includes VERTEX.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('shaderStage', kShaderStageCombinations)
+ .beginSubcases()
+ .combine('type', kBufferBindingTypes)
+ )
+ .fn(async t => {
+ const { shaderStage, type } = t.params;
+
+ const success = !(type === 'storage' && shaderStage & GPUShaderStage.VERTEX);
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: shaderStage,
+ buffer: { type },
+ },
+ ],
+ });
+ }, !success);
+ });
+
+g.test('visibility,VERTEX_shader_stage_storage_texture_access')
+ .desc(
+ `
+ Test that a validation error is generated if the access value is 'write-only' when the
+ visibility of the entry includes VERTEX.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('shaderStage', kShaderStageCombinations)
+ .beginSubcases()
+ .combine('access', [undefined, ...kStorageTextureAccessValues])
+ )
+ .fn(async t => {
+ const { shaderStage, access } = t.params;
+
+ const success = !(
+ (access ?? 'write-only') === 'write-only' && shaderStage & GPUShaderStage.VERTEX
+ );
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: shaderStage,
+ storageTexture: { access, format: 'rgba8unorm' },
+ },
+ ],
+ });
+ }, !success);
+ });
+
+g.test('multisampled_validation')
+ .desc(
+ `
+ Test that multisampling is only allowed if view dimensions is "2d" and the sampleType is not
+ "float".
+ `
+ )
+ .params(u =>
+ u //
+ .combine('viewDimension', [undefined, ...kTextureViewDimensions])
+ .beginSubcases()
+ .combine('sampleType', [undefined, ...kTextureSampleTypes])
+ )
+ .fn(async t => {
+ const { viewDimension, sampleType } = t.params;
+
+ const success =
+ (viewDimension === '2d' || viewDimension === undefined) &&
+ (sampleType ?? 'float') !== 'float';
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ texture: { multisampled: true, viewDimension, sampleType },
+ },
+ ],
+ });
+ }, !success);
+ });
+
+g.test('max_dynamic_buffers')
+ .desc(
+ `
+ Test that limits on the maximum number of dynamic buffers are enforced.
+ - Test creation of a bind group layout using the maximum number of dynamic buffers works.
+ - Test creation of a bind group layout using the maximum number of dynamic buffers + 1 fails.
+ - TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.`
+ )
+ .params(u =>
+ u
+ .combine('type', kBufferBindingTypes)
+ .beginSubcases()
+ .combine('extraDynamicBuffers', [0, 1])
+ .combine('staticBuffers', [0, 1])
+ )
+ .fn(async t => {
+ const { type, extraDynamicBuffers, staticBuffers } = t.params;
+ const info = bufferBindingTypeInfo({ type });
+
+ const dynamicBufferCount = info.perPipelineLimitClass.maxDynamic + extraDynamicBuffers;
+
+ const entries = [];
+ for (let i = 0; i < dynamicBufferCount; i++) {
+ entries.push({
+ binding: i,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type, hasDynamicOffset: true },
+ });
+ }
+
+ for (let i = dynamicBufferCount; i < dynamicBufferCount + staticBuffers; i++) {
+ entries.push({
+ binding: i,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type, hasDynamicOffset: false },
+ });
+ }
+
+ const descriptor = {
+ entries,
+ };
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout(descriptor);
+ }, extraDynamicBuffers > 0);
+ });
+
+/**
+ * One bind group layout will be filled with kPerStageBindingLimit[...] of the type |type|.
+ * For each item in the array returned here, a case will be generated which tests a pipeline
+ * layout with one extra bind group layout with one extra binding. That extra binding will have:
+ *
+ * - If extraTypeSame, any of the binding types which counts toward the same limit as |type|.
+ * (i.e. 'storage-buffer' <-> 'readonly-storage-buffer').
+ * - Otherwise, an arbitrary other type.
+ */
+function* pickExtraBindingTypesForPerStage(entry: BGLEntry, extraTypeSame: boolean) {
+ if (extraTypeSame) {
+ const info = bindingTypeInfo(entry);
+ for (const extra of allBindingEntries(false)) {
+ const extraInfo = bindingTypeInfo(extra);
+ if (info.perStageLimitClass.class === extraInfo.perStageLimitClass.class) {
+ yield extra;
+ }
+ }
+ } else {
+ yield entry.sampler ? { texture: {} } : { sampler: {} };
+ }
+}
+
+const kMaxResourcesCases = kUnitCaseParamsBuilder
+ .combine('maxedEntry', allBindingEntries(false))
+ .beginSubcases()
+ .combine('maxedVisibility', kShaderStages)
+ .filter(p => (bindingTypeInfo(p.maxedEntry).validStages & p.maxedVisibility) !== 0)
+ .expand('extraEntry', p => [
+ ...pickExtraBindingTypesForPerStage(p.maxedEntry, true),
+ ...pickExtraBindingTypesForPerStage(p.maxedEntry, false),
+ ])
+ .combine('extraVisibility', kShaderStages)
+ .filter(p => (bindingTypeInfo(p.extraEntry).validStages & p.extraVisibility) !== 0);
+
+// Should never fail unless kMaxBindingsPerBindGroup is exceeded, because the validation for
+// resources-of-type-per-stage is in pipeline layout creation.
+g.test('max_resources_per_stage,in_bind_group_layout')
+ .desc(
+ `
+ Test that the maximum number of bindings of a given type per-stage cannot be exceeded in a
+ single bind group layout.
+ - Test each binding type.
+ - Test that creation of a bind group layout using the maximum number of bindings works.
+ - Test that creation of a bind group layout using the maximum number of bindings + 1 fails.
+ - TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.`
+ )
+ .params(kMaxResourcesCases)
+ .fn(async t => {
+ const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
+ const maxedTypeInfo = bindingTypeInfo(maxedEntry);
+ const maxedCount = maxedTypeInfo.perStageLimitClass.max;
+ const extraTypeInfo = bindingTypeInfo(extraEntry);
+
+ const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
+ for (let i = 0; i < maxedCount; i++) {
+ maxResourceBindings.push({
+ binding: i,
+ visibility: maxedVisibility,
+ ...maxedEntry,
+ });
+ }
+
+ const goodDescriptor = { entries: maxResourceBindings };
+
+ // Control
+ t.device.createBindGroupLayout(goodDescriptor);
+
+ // Add an entry counting towards the same limit. It should produce a validation error.
+ const newDescriptor = clone(goodDescriptor);
+ newDescriptor.entries.push({
+ binding: maxedCount,
+ visibility: extraVisibility,
+ ...extraEntry,
+ });
+
+ const newBindingCountsTowardSamePerStageLimit =
+ (maxedVisibility & extraVisibility) !== 0 &&
+ maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class;
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout(newDescriptor);
+ }, newBindingCountsTowardSamePerStageLimit);
+ });
+
+// One pipeline layout can have a maximum number of each type of binding *per stage* (which is
+// different for each type). Test that the max works, then add one more binding of same-or-different
+// type and same-or-different visibility.
+g.test('max_resources_per_stage,in_pipeline_layout')
+ .desc(
+ `
+ Test that the maximum number of bindings of a given type per-stage cannot be exceeded across
+ multiple bind group layouts when creating a pipeline layout.
+ - Test each binding type.
+ - Test that creation of a pipeline using the maximum number of bindings works.
+ - Test that creation of a pipeline using the maximum number of bindings + 1 fails.
+ `
+ )
+ .params(kMaxResourcesCases)
+ .fn(async t => {
+ const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
+ const maxedTypeInfo = bindingTypeInfo(maxedEntry);
+ const maxedCount = maxedTypeInfo.perStageLimitClass.max;
+ const extraTypeInfo = bindingTypeInfo(extraEntry);
+
+ const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
+ for (let i = 0; i < maxedCount; i++) {
+ maxResourceBindings.push({
+ binding: i,
+ visibility: maxedVisibility,
+ ...maxedEntry,
+ });
+ }
+
+ const goodLayout = t.device.createBindGroupLayout({ entries: maxResourceBindings });
+
+ // Control
+ t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout] });
+
+ const extraLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: extraVisibility,
+ ...extraEntry,
+ },
+ ],
+ });
+
+ // Some binding types use the same limit, e.g. 'storage-buffer' and 'readonly-storage-buffer'.
+ const newBindingCountsTowardSamePerStageLimit =
+ (maxedVisibility & extraVisibility) !== 0 &&
+ maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class;
+
+ t.expectValidationError(() => {
+ t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout, extraLayout] });
+ }, newBindingCountsTowardSamePerStageLimit);
+ });
+
+g.test('storage_texture,layout_dimension')
+ .desc(
+ `
+ Test that viewDimension is not cube or cube-array if storageTextureLayout is not undefined.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('viewDimension', [undefined, ...kTextureViewDimensions])
+ )
+ .fn(async t => {
+ const { viewDimension } = t.params;
+
+ const success = viewDimension !== 'cube' && viewDimension !== `cube-array`;
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: { format: 'rgba8unorm', viewDimension },
+ },
+ ],
+ });
+ }, !success);
+ });
+
+g.test('storage_texture,formats')
+ .desc(
+ `
+ Test that a validation error is generated if the format doesn't support the storage usage.
+
+ TODO: Test "bgra8unorm" with the "bgra8unorm-storage" feature.
+ `
+ )
+ .params(u => u.combine('format', kAllTextureFormats))
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: { format },
+ },
+ ],
+ });
+ }, !info.storage);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createPipelineLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createPipelineLayout.spec.ts
new file mode 100644
index 0000000000..3e2f31bd76
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createPipelineLayout.spec.ts
@@ -0,0 +1,156 @@
+export const description = `
+createPipelineLayout validation tests.
+
+TODO: review existing tests, write descriptions, and make sure tests are complete.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { bufferBindingTypeInfo, kBufferBindingTypes } from '../../capability_info.js';
+
+import { ValidationTest } from './validation_test.js';
+
+function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
+ return JSON.parse(JSON.stringify(descriptor));
+}
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('number_of_dynamic_buffers_exceeds_the_maximum_value')
+ .desc(
+ `
+ Test that creating a pipeline layout fails with a validation error if the number of dynamic
+ buffers exceeds the maximum value in the pipeline layout.
+ - Test that creation of a pipeline using the maximum number of dynamic buffers added a dynamic
+ buffer fails.
+
+ TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('visibility', [0, 2, 4, 6])
+ .combine('type', kBufferBindingTypes)
+ )
+ .fn(async t => {
+ const { type, visibility } = t.params;
+ const { maxDynamic } = bufferBindingTypeInfo({ type }).perPipelineLimitClass;
+
+ const maxDynamicBufferBindings: GPUBindGroupLayoutEntry[] = [];
+ for (let binding = 0; binding < maxDynamic; binding++) {
+ maxDynamicBufferBindings.push({
+ binding,
+ visibility,
+ buffer: { type, hasDynamicOffset: true },
+ });
+ }
+
+ const maxDynamicBufferBindGroupLayout = t.device.createBindGroupLayout({
+ entries: maxDynamicBufferBindings,
+ });
+
+ const goodDescriptor = {
+ entries: [{ binding: 0, visibility, buffer: { type, hasDynamicOffset: false } }],
+ };
+
+ const goodPipelineLayoutDescriptor = {
+ bindGroupLayouts: [
+ maxDynamicBufferBindGroupLayout,
+ t.device.createBindGroupLayout(goodDescriptor),
+ ],
+ };
+
+ // Control case
+ t.device.createPipelineLayout(goodPipelineLayoutDescriptor);
+
+ // Check dynamic buffers exceed maximum in pipeline layout.
+ const badDescriptor = clone(goodDescriptor);
+ badDescriptor.entries[0].buffer.hasDynamicOffset = true;
+
+ const badPipelineLayoutDescriptor = {
+ bindGroupLayouts: [
+ maxDynamicBufferBindGroupLayout,
+ t.device.createBindGroupLayout(badDescriptor),
+ ],
+ };
+
+ t.expectValidationError(() => {
+ t.device.createPipelineLayout(badPipelineLayoutDescriptor);
+ });
+ });
+
+g.test('number_of_bind_group_layouts_exceeds_the_maximum_value')
+ .desc(
+ `
+ Test that creating a pipeline layout fails with a validation error if the number of bind group
+ layouts exceeds the maximum value in the pipeline layout.
+ - Test that creation of a pipeline using the maximum number of bind groups added a bind group
+ fails.
+ `
+ )
+ .fn(async t => {
+ const bindGroupLayoutDescriptor: GPUBindGroupLayoutDescriptor = {
+ entries: [],
+ };
+
+ // 4 is the maximum number of bind group layouts.
+ const maxBindGroupLayouts = [1, 2, 3, 4].map(() =>
+ t.device.createBindGroupLayout(bindGroupLayoutDescriptor)
+ );
+
+ const goodPipelineLayoutDescriptor = {
+ bindGroupLayouts: maxBindGroupLayouts,
+ };
+
+ // Control case
+ t.device.createPipelineLayout(goodPipelineLayoutDescriptor);
+
+ // Check bind group layouts exceed maximum in pipeline layout.
+ const badPipelineLayoutDescriptor = {
+ bindGroupLayouts: [
+ ...maxBindGroupLayouts,
+ t.device.createBindGroupLayout(bindGroupLayoutDescriptor),
+ ],
+ };
+
+ t.expectValidationError(() => {
+ t.device.createPipelineLayout(badPipelineLayoutDescriptor);
+ });
+ });
+
+g.test('bind_group_layouts,device_mismatch')
+ .desc(
+ `
+ Tests createPipelineLayout cannot be called with bind group layouts created from another device
+ Test with two layouts to make sure all layouts can be validated:
+ - layout0 and layout1 from same device
+ - layout0 and layout1 from different device
+ `
+ )
+ .paramsSubcasesOnly([
+ { layout0Mismatched: false, layout1Mismatched: false }, // control case
+ { layout0Mismatched: true, layout1Mismatched: false },
+ { layout0Mismatched: false, layout1Mismatched: true },
+ ])
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { layout0Mismatched, layout1Mismatched } = t.params;
+
+ const mismatched = layout0Mismatched || layout1Mismatched;
+
+ const bglDescriptor: GPUBindGroupLayoutDescriptor = {
+ entries: [],
+ };
+
+ const layout0 = layout0Mismatched
+ ? t.mismatchedDevice.createBindGroupLayout(bglDescriptor)
+ : t.device.createBindGroupLayout(bglDescriptor);
+ const layout1 = layout1Mismatched
+ ? t.mismatchedDevice.createBindGroupLayout(bglDescriptor)
+ : t.device.createBindGroupLayout(bglDescriptor);
+
+ t.expectValidationError(() => {
+ t.device.createPipelineLayout({ bindGroupLayouts: [layout0, layout1] });
+ }, mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createSampler.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createSampler.spec.ts
new file mode 100644
index 0000000000..1e379fed26
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createSampler.spec.ts
@@ -0,0 +1,59 @@
+export const description = `
+createSampler validation tests.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+import { ValidationTest } from './validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('lodMinAndMaxClamp')
+ .desc('test different combinations of min and max clamp values')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('lodMinClamp', [-4e-30, -1, 0, 0.5, 1, 10, 4e30])
+ .combine('lodMaxClamp', [-4e-30, -1, 0, 0.5, 1, 10, 4e30])
+ )
+ .fn(async t => {
+ t.expectValidationError(() => {
+ t.device.createSampler({
+ lodMinClamp: t.params.lodMinClamp,
+ lodMaxClamp: t.params.lodMaxClamp,
+ });
+ }, t.params.lodMinClamp > t.params.lodMaxClamp || t.params.lodMinClamp < 0 || t.params.lodMaxClamp < 0);
+ });
+
+g.test('maxAnisotropy')
+ .desc('test different maxAnisotropy values and combinations with min/mag/mipmapFilter')
+ .params(u =>
+ u //
+ .beginSubcases()
+ .combineWithParams([
+ ...u.combine('maxAnisotropy', [-1, undefined, 0, 1, 2, 4, 7, 16, 32, 33, 1024]),
+ { minFilter: 'nearest' as const },
+ { magFilter: 'nearest' as const },
+ { mipmapFilter: 'nearest' as const },
+ ])
+ )
+ .fn(async t => {
+ const {
+ maxAnisotropy = 1,
+ minFilter = 'linear',
+ magFilter = 'linear',
+ mipmapFilter = 'linear',
+ } = t.params as {
+ maxAnisotropy?: number;
+ minFilter?: GPUFilterMode;
+ magFilter?: GPUFilterMode;
+ mipmapFilter?: GPUFilterMode;
+ };
+ t.expectValidationError(() => {
+ t.device.createSampler({
+ minFilter,
+ magFilter,
+ mipmapFilter,
+ maxAnisotropy,
+ });
+ }, maxAnisotropy < 1 || (maxAnisotropy > 1 && !(minFilter === 'linear' && magFilter === 'linear' && mipmapFilter === 'linear')));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts
new file mode 100644
index 0000000000..5628d7a20c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createTexture.spec.ts
@@ -0,0 +1,879 @@
+export const description = `createTexture validation tests.`;
+
+import { SkipTestCase } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import {
+ kTextureFormats,
+ kTextureFormatInfo,
+ kCompressedTextureFormats,
+ kTextureDimensions,
+ kTextureUsages,
+ kUncompressedTextureFormats,
+ kRegularTextureFormats,
+ kFeaturesForFormats,
+ textureDimensionAndFormatCompatible,
+ kLimitInfo,
+ viewCompatible,
+ filterFormatsByFeature,
+} from '../../capability_info.js';
+import { GPUConst } from '../../constants.js';
+import { maxMipLevelCount } from '../../util/texture/base.js';
+
+import { ValidationTest } from './validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('zero_size_and_usage')
+ .desc(
+ `Test texture creation with zero or nonzero size of
+ width, height, depthOrArrayLayers and mipLevelCount, usage for every dimension, and
+ representative formats.
+ `
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, ...kTextureDimensions])
+ .combine('format', [
+ 'rgba8unorm',
+ 'rgb10a2unorm',
+ 'bc1-rgba-unorm',
+ 'depth24plus-stencil8',
+ ] as const)
+ .beginSubcases()
+ .combine('zeroArgument', [
+ 'none',
+ 'width',
+ 'height',
+ 'depthOrArrayLayers',
+ 'mipLevelCount',
+ 'usage',
+ ] as const)
+ // Filter out incompatible dimension type and format combinations.
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, zeroArgument, format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const size = [info.blockWidth, info.blockHeight, 1];
+ let mipLevelCount = 1;
+ let usage = GPUTextureUsage.TEXTURE_BINDING;
+
+ switch (zeroArgument) {
+ case 'width':
+ size[0] = 0;
+ break;
+ case 'height':
+ size[1] = 0;
+ break;
+ case 'depthOrArrayLayers':
+ size[2] = 0;
+ break;
+ case 'mipLevelCount':
+ mipLevelCount = 0;
+ break;
+ case 'usage':
+ usage = 0;
+ break;
+ default:
+ break;
+ }
+
+ const descriptor = {
+ size,
+ mipLevelCount,
+ dimension,
+ format,
+ usage,
+ };
+
+ const success = zeroArgument === 'none';
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('dimension_type_and_format_compatibility')
+ .desc(
+ `Test every dimension type on every format. Note that compressed formats and depth/stencil formats are not valid for 1D/3D dimension types.`
+ )
+ .params(u =>
+ u.combine('dimension', [undefined, ...kTextureDimensions]).combine('format', kTextureFormats)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor: GPUTextureDescriptor = {
+ size: [info.blockWidth, info.blockHeight, 1],
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !textureDimensionAndFormatCompatible(dimension, format));
+ });
+
+g.test('mipLevelCount,format')
+ .desc(
+ `Test texture creation with no mipmap chain, partial mipmap chain, full mipmap chain, out-of-bounds mipmap chain
+ for every format with different texture dimension types.`
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, ...kTextureDimensions])
+ .combine('format', kTextureFormats)
+ .beginSubcases()
+ .combine('mipLevelCount', [1, 2, 3, 6, 7])
+ // Filter out incompatible dimension type and format combinations.
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .combine('largestDimension', [0, 1, 2])
+ .unless(({ dimension, largestDimension }) => dimension === '1d' && largestDimension > 0)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, mipLevelCount, largestDimension } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ // Compute dimensions such that the dimensions are in range [17, 32] and aligned with the
+ // format block size so that there will be exactly 6 mip levels.
+ const kTargetMipLevelCount = 5;
+ const kTargetLargeSize = (1 << kTargetMipLevelCount) - 1;
+ const largeSize = [
+ Math.floor(kTargetLargeSize / info.blockWidth) * info.blockWidth,
+ Math.floor(kTargetLargeSize / info.blockHeight) * info.blockHeight,
+ kTargetLargeSize,
+ ];
+ assert(17 <= largeSize[0] && largeSize[0] <= 32);
+ assert(17 <= largeSize[1] && largeSize[1] <= 32);
+
+ // Note that compressed formats are not valid for 1D. They have already been filtered out for 1D
+ // in this test. So there is no dilemma about size.width equals 1 vs
+ // size.width % info.blockHeight equals 0 for 1D compressed formats.
+ const size = [info.blockWidth, info.blockHeight, 1];
+ size[largestDimension] = largeSize[largestDimension];
+
+ const descriptor = {
+ size,
+ mipLevelCount,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success = mipLevelCount <= maxMipLevelCount(descriptor);
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('mipLevelCount,bound_check')
+ .desc(
+ `Test mip level count bound check upon different texture size and different texture dimension types.
+ The cases below test: 1) there must be no mip levels after a 1 level (1D texture), or 1x1 level (2D texture), or 1x1x1 level (3D texture), 2) array layers are not mip-mapped, 3) power-of-two, non-power-of-two, and non-square sizes.`
+ )
+ .params(u =>
+ u //
+ .combine('format', ['rgba8unorm', 'bc1-rgba-unorm'] as const)
+ .beginSubcases()
+ .combineWithParams([
+ { size: [32, 32] }, // Mip level sizes: 32x32, 16x16, 8x8, 4x4, 2x2, 1x1
+ { size: [31, 32] }, // Mip level sizes: 31x32, 15x16, 7x8, 3x4, 1x2, 1x1
+ { size: [28, 32] }, // Mip level sizes: 28x32, 14x16, 7x8, 3x4, 1x2, 1x1
+ { size: [32, 31] }, // Mip level sizes: 32x31, 16x15, 8x7, 4x3, 2x1, 1x1
+ { size: [32, 28] }, // Mip level sizes: 32x28, 16x14, 8x7, 4x3, 2x1, 1x1
+ { size: [31, 31] }, // Mip level sizes: 31x31, 15x15, 7x7, 3x3, 1x1
+ { size: [32], dimension: '1d' as const }, // Mip level sizes: 32, 16, 8, 4, 2, 1
+ { size: [31], dimension: '1d' as const }, // Mip level sizes: 31, 15, 7, 3, 1
+ { size: [32, 32, 32], dimension: '3d' as const }, // Mip level sizes: 32x32x32, 16x16x16, 8x8x8, 4x4x4, 2x2x2, 1x1x1
+ { size: [32, 31, 31], dimension: '3d' as const }, // Mip level sizes: 32x31x31, 16x15x15, 8x7x7, 4x3x3, 2x1x1, 1x1x1
+ { size: [31, 32, 31], dimension: '3d' as const }, // Mip level sizes: 31x32x31, 15x16x15, 7x8x7, 3x4x3, 1x2x1, 1x1x1
+ { size: [31, 31, 32], dimension: '3d' as const }, // Mip level sizes: 31x31x32, 15x15x16, 7x7x8, 3x3x4, 1x1x2, 1x1x1
+ { size: [31, 31, 31], dimension: '3d' as const }, // Mip level sizes: 31x31x31, 15x15x15, 7x7x7, 3x3x3, 1x1x1
+ { size: [32, 8] }, // Mip levels: 32x8, 16x4, 8x2, 4x1, 2x1, 1x1
+ { size: [32, 32, 64] }, // Mip levels: 32x32x64, 16x16x64, 8x8x64, 4x4x64, 2x2x64, 1x1x64
+ { size: [32, 32, 64], dimension: '3d' as const }, // Mip levels: 32x32x64, 16x16x32, 8x8x16, 4x4x8, 2x2x4, 1x1x2, 1x1x1
+ ])
+ .unless(
+ ({ format, size, dimension }) =>
+ format === 'bc1-rgba-unorm' &&
+ (dimension === '1d' ||
+ dimension === '3d' ||
+ size[0] % kTextureFormatInfo[format].blockWidth !== 0 ||
+ size[1] % kTextureFormatInfo[format].blockHeight !== 0)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, size, dimension } = t.params;
+
+ const descriptor = {
+ size,
+ mipLevelCount: 0,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const mipLevelCount = maxMipLevelCount(descriptor);
+ descriptor.mipLevelCount = mipLevelCount;
+ t.device.createTexture(descriptor);
+
+ descriptor.mipLevelCount = mipLevelCount + 1;
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ });
+ });
+
+g.test('mipLevelCount,bound_check,bigger_than_integer_bit_width')
+ .desc(`Test mip level count bound check when mipLevelCount is bigger than integer bit width`)
+ .fn(async t => {
+ const descriptor = {
+ size: [32, 32],
+ mipLevelCount: 100,
+ format: 'rgba8unorm' as const,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ });
+ });
+
+g.test('sampleCount,various_sampleCount_with_all_formats')
+ .desc(
+ `Test texture creation with various (valid or invalid) sample count and all formats. Note that 1D and 3D textures can't support multisample.`
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, '2d'] as const)
+ .combine('format', kTextureFormats)
+ .beginSubcases()
+ .combine('sampleCount', [0, 1, 2, 4, 8, 16, 32, 256])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, sampleCount, format } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+
+ const usage =
+ sampleCount > 1
+ ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
+ : GPUTextureUsage.TEXTURE_BINDING;
+ const descriptor = {
+ size: [32 * blockWidth, 32 * blockHeight, 1],
+ sampleCount,
+ dimension,
+ format,
+ usage,
+ };
+
+ const success =
+ sampleCount === 1 ||
+ (sampleCount === 4 &&
+ kTextureFormatInfo[format].multisample &&
+ kTextureFormatInfo[format].renderable);
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('sampleCount,valid_sampleCount_with_other_parameter_varies')
+ .desc(
+ `Test texture creation with valid sample count when dimensions, arrayLayerCount, mipLevelCount,
+ format, and usage varies. Texture can be single sample (sampleCount is 1) or multi-sample
+ (sampleCount is 4). Multisample texture requires that
+ 1) its dimension is 2d or undefined,
+ 2) its format supports multisample,
+ 3) its mipLevelCount and arrayLayerCount are 1,
+ 4) its usage doesn't include STORAGE_BINDING,
+ 5) its usage includes RENDER_ATTACHMENT.`
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, ...kTextureDimensions])
+ .combine('format', kTextureFormats)
+ .beginSubcases()
+ .combine('sampleCount', [1, 4])
+ .combine('arrayLayerCount', [1, 2])
+ .unless(
+ ({ dimension, arrayLayerCount }) =>
+ arrayLayerCount === 2 && dimension !== '2d' && dimension !== undefined
+ )
+ .combine('mipLevelCount', [1, 2])
+ .expand('usage', p => {
+ const usageSet = new Set<number>();
+ for (const usage0 of kTextureUsages) {
+ for (const usage1 of kTextureUsages) {
+ usageSet.add(usage0 | usage1);
+ }
+ }
+ return usageSet;
+ })
+ // Filter out incompatible dimension type and format combinations.
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .unless(({ usage, format, mipLevelCount, dimension }) => {
+ const info = kTextureFormatInfo[format];
+ return (
+ ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
+ (!info.renderable || dimension !== '2d')) ||
+ ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.storage) ||
+ (mipLevelCount !== 1 && dimension === '1d')
+ );
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, sampleCount, format, mipLevelCount, arrayLayerCount, usage } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+
+ const size =
+ dimension === '1d'
+ ? [32 * blockWidth, 1 * blockHeight, 1]
+ : dimension === '2d' || dimension === undefined
+ ? [32 * blockWidth, 32 * blockHeight, arrayLayerCount]
+ : [32 * blockWidth, 32 * blockHeight, 32];
+ const descriptor = {
+ size,
+ mipLevelCount,
+ sampleCount,
+ dimension,
+ format,
+ usage,
+ };
+
+ const success =
+ sampleCount === 1 ||
+ (sampleCount === 4 &&
+ (dimension === '2d' || dimension === undefined) &&
+ kTextureFormatInfo[format].multisample &&
+ mipLevelCount === 1 &&
+ arrayLayerCount === 1 &&
+ (usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
+ (usage & GPUConst.TextureUsage.STORAGE_BINDING) === 0);
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('sample_count,1d_2d_array_3d')
+ .desc(`Test that you can not create 1d, 2d_array, and 3d multisampled textures`)
+ .params(u =>
+ u.combineWithParams([
+ { dimension: '2d', size: [4, 4, 1], shouldError: false },
+ { dimension: '1d', size: [4, 1, 1], shouldError: true },
+ { dimension: '2d', size: [4, 4, 4], shouldError: true },
+ { dimension: '2d', size: [4, 4, 6], shouldError: true },
+ { dimension: '3d', size: [4, 4, 4], shouldError: true },
+ ] as const)
+ )
+ .fn(async t => {
+ const { dimension, size, shouldError } = t.params;
+
+ t.expectValidationError(() => {
+ t.device.createTexture({
+ size,
+ dimension,
+ sampleCount: 4,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ }, shouldError);
+ });
+
+g.test('texture_size,default_value_and_smallest_size,uncompressed_format')
+ .desc(
+ `Test default values for height and depthOrArrayLayers for every dimension type and every uncompressed format.
+ It also tests smallest size (lower bound) for every dimension type and every uncompressed format, while other texture_size tests are testing the upper bound.`
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, ...kTextureDimensions])
+ .combine('format', kUncompressedTextureFormats)
+ .beginSubcases()
+ .combine('size', [[1], [1, 1], [1, 1, 1]])
+ // Filter out incompatible dimension type and format combinations.
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, size } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ t.device.createTexture(descriptor);
+ });
+
+g.test('texture_size,default_value_and_smallest_size,compressed_format')
+ .desc(
+ `Test default values for height and depthOrArrayLayers for every dimension type and every compressed format.
+ It also tests smallest size (lower bound) for every dimension type and every compressed format, while other texture_size tests are testing the upper bound.`
+ )
+ .params(u =>
+ u
+ // Compressed formats are invalid for 1D and 3D.
+ .combine('dimension', [undefined, '2d'] as const)
+ .combine('format', kCompressedTextureFormats)
+ .beginSubcases()
+ .expandWithParams(p => {
+ const { blockWidth, blockHeight } = kTextureFormatInfo[p.format];
+ return [
+ { size: [1], _success: false },
+ { size: [blockWidth], _success: false },
+ { size: [1, 1], _success: false },
+ { size: [blockWidth, blockHeight], _success: true },
+ { size: [1, 1, 1], _success: false },
+ { size: [blockWidth, blockHeight, 1], _success: true },
+ ];
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, size, _success } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !_success);
+ });
+
+g.test('texture_size,1d_texture')
+ .desc(`Test texture size requirement for 1D texture`)
+ .params(u =>
+ u //
+ // Compressed and depth-stencil textures are invalid for 1D.
+ .combine('format', kRegularTextureFormats)
+ .beginSubcases()
+ .combine('width', [
+ kLimitInfo.maxTextureDimension1D.default - 1,
+ kLimitInfo.maxTextureDimension1D.default,
+ kLimitInfo.maxTextureDimension1D.default + 1,
+ ])
+ .combine('height', [1, 2])
+ .combine('depthOrArrayLayers', [1, 2])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, width, height, depthOrArrayLayers } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size: [width, height, depthOrArrayLayers],
+ dimension: '1d' as const,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success =
+ width <= kLimitInfo.maxTextureDimension1D.default && height === 1 && depthOrArrayLayers === 1;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('texture_size,2d_texture,uncompressed_format')
+ .desc(`Test texture size requirement for 2D texture with uncompressed format.`)
+ .params(u =>
+ u
+ .combine('dimension', [undefined, '2d'] as const)
+ .combine('format', kUncompressedTextureFormats)
+ .combine('size', [
+ // Test the bound of width
+ [kLimitInfo.maxTextureDimension2D.default - 1, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default + 1, 1, 1],
+ // Test the bound of height
+ [1, kLimitInfo.maxTextureDimension2D.default - 1, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default + 1, 1],
+ // Test the bound of array layers
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default - 1],
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default],
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default + 1],
+ ])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, size } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success =
+ size[0] <= kLimitInfo.maxTextureDimension2D.default &&
+ size[1] <= kLimitInfo.maxTextureDimension2D.default &&
+ size[2] <= kLimitInfo.maxTextureArrayLayers.default;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('texture_size,2d_texture,compressed_format')
+ .desc(`Test texture size requirement for 2D texture with compressed format.`)
+ .params(u =>
+ u
+ .combine('dimension', [undefined, '2d'] as const)
+ .combine('format', kCompressedTextureFormats)
+ .expand('size', p => {
+ const { blockWidth, blockHeight } = kTextureFormatInfo[p.format];
+ return [
+ // Test the bound of width
+ [kLimitInfo.maxTextureDimension2D.default - 1, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default - blockWidth, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default - blockWidth, blockHeight, 1],
+ [kLimitInfo.maxTextureDimension2D.default, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default, blockHeight, 1],
+ [kLimitInfo.maxTextureDimension2D.default + 1, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default + blockWidth, 1, 1],
+ [kLimitInfo.maxTextureDimension2D.default + blockWidth, blockHeight, 1],
+ // Test the bound of height
+ [1, kLimitInfo.maxTextureDimension2D.default - 1, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default - blockHeight, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension2D.default - blockHeight, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension2D.default, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default + 1, 1],
+ [1, kLimitInfo.maxTextureDimension2D.default + blockWidth, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension2D.default + blockHeight, 1],
+ // Test the bound of array layers
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default - 1],
+ [blockWidth, 1, kLimitInfo.maxTextureArrayLayers.default - 1],
+ [1, blockHeight, kLimitInfo.maxTextureArrayLayers.default - 1],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureArrayLayers.default - 1],
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default],
+ [blockWidth, 1, kLimitInfo.maxTextureArrayLayers.default],
+ [1, blockHeight, kLimitInfo.maxTextureArrayLayers.default],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureArrayLayers.default],
+ [1, 1, kLimitInfo.maxTextureArrayLayers.default + 1],
+ [blockWidth, 1, kLimitInfo.maxTextureArrayLayers.default + 1],
+ [1, blockHeight, kLimitInfo.maxTextureArrayLayers.default + 1],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureArrayLayers.default + 1],
+ ];
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, size } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success =
+ size[0] % info.blockWidth === 0 &&
+ size[1] % info.blockHeight === 0 &&
+ size[0] <= kLimitInfo.maxTextureDimension2D.default &&
+ size[1] <= kLimitInfo.maxTextureDimension2D.default &&
+ size[2] <= kLimitInfo.maxTextureArrayLayers.default;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('texture_size,3d_texture,uncompressed_format')
+ .desc(
+ `Test texture size requirement for 3D texture with uncompressed format. Note that depth/stencil formats are invalid for 3D textures, so we only test regular formats.`
+ )
+ .params(u =>
+ u //
+ .combine('format', kRegularTextureFormats)
+ .beginSubcases()
+ .combine('size', [
+ // Test the bound of width
+ [kLimitInfo.maxTextureDimension3D.default - 1, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default + 1, 1, 1],
+ // Test the bound of height
+ [1, kLimitInfo.maxTextureDimension3D.default - 1, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default + 1, 1],
+ // Test the bound of depth
+ [1, 1, kLimitInfo.maxTextureDimension3D.default - 1],
+ [1, 1, kLimitInfo.maxTextureDimension3D.default],
+ [1, 1, kLimitInfo.maxTextureDimension3D.default + 1],
+ ])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, size } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension: '3d' as const,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success =
+ size[0] <= kLimitInfo.maxTextureDimension3D.default &&
+ size[1] <= kLimitInfo.maxTextureDimension3D.default &&
+ size[2] <= kLimitInfo.maxTextureDimension3D.default;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('texture_size,3d_texture,compressed_format')
+ .desc(`Test texture size requirement for 3D texture with compressed format.`)
+ .params(u =>
+ u //
+ .combine('format', kCompressedTextureFormats)
+ .beginSubcases()
+ .expand('size', p => {
+ const { blockWidth, blockHeight } = kTextureFormatInfo[p.format];
+ return [
+ // Test the bound of width
+ [kLimitInfo.maxTextureDimension3D.default - 1, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default - blockWidth, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default - blockWidth, blockHeight, 1],
+ [kLimitInfo.maxTextureDimension3D.default, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default, blockHeight, 1],
+ [kLimitInfo.maxTextureDimension3D.default + 1, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default + blockWidth, 1, 1],
+ [kLimitInfo.maxTextureDimension3D.default + blockWidth, blockHeight, 1],
+ // Test the bound of height
+ [1, kLimitInfo.maxTextureDimension3D.default - 1, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default - blockHeight, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension3D.default - blockHeight, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension3D.default, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default + 1, 1],
+ [1, kLimitInfo.maxTextureDimension3D.default + blockWidth, 1],
+ [blockWidth, kLimitInfo.maxTextureDimension3D.default + blockHeight, 1],
+ // Test the bound of depth
+ [1, 1, kLimitInfo.maxTextureDimension3D.default - 1],
+ [blockWidth, 1, kLimitInfo.maxTextureDimension3D.default - 1],
+ [1, blockHeight, kLimitInfo.maxTextureDimension3D.default - 1],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureDimension3D.default - 1],
+ [1, 1, kLimitInfo.maxTextureDimension3D.default],
+ [blockWidth, 1, kLimitInfo.maxTextureDimension3D.default],
+ [1, blockHeight, kLimitInfo.maxTextureDimension3D.default],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureDimension3D.default],
+ [1, 1, kLimitInfo.maxTextureDimension3D.default + 1],
+ [blockWidth, 1, kLimitInfo.maxTextureDimension3D.default + 1],
+ [1, blockHeight, kLimitInfo.maxTextureDimension3D.default + 1],
+ [blockWidth, blockHeight, kLimitInfo.maxTextureDimension3D.default + 1],
+ ];
+ })
+ )
+ .beforeAllSubcases(t => {
+ // Compressed formats are not supported in 3D in WebGPU v1 because they are complicated but not very useful for now.
+ throw new SkipTestCase('Compressed 3D texture is not supported');
+
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, size } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ assert(
+ kLimitInfo.maxTextureDimension3D.default % info.blockWidth === 0 &&
+ kLimitInfo.maxTextureDimension3D.default % info.blockHeight === 0
+ );
+
+ const descriptor: GPUTextureDescriptor = {
+ size,
+ dimension: '3d' as const,
+ format,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const success =
+ size[0] % info.blockWidth === 0 &&
+ size[1] % info.blockHeight === 0 &&
+ size[0] <= kLimitInfo.maxTextureDimension3D.default &&
+ size[1] <= kLimitInfo.maxTextureDimension3D.default &&
+ size[2] <= kLimitInfo.maxTextureDimension3D.default;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('texture_usage')
+ .desc(
+ `Test texture usage (single usage or combined usages) for every texture format and every dimension type`
+ )
+ .params(u =>
+ u
+ .combine('dimension', [undefined, ...kTextureDimensions])
+ .combine('format', kTextureFormats)
+ .beginSubcases()
+ // If usage0 and usage1 are the same, then the usage being test is a single usage. Otherwise, it is a combined usage.
+ .combine('usage0', kTextureUsages)
+ .combine('usage1', kTextureUsages)
+ // Filter out incompatible dimension type and format combinations.
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { dimension, format, usage0, usage1 } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const size = [info.blockWidth, info.blockHeight, 1];
+ const usage = usage0 | usage1;
+ const descriptor = {
+ size,
+ dimension,
+ format,
+ usage,
+ };
+
+ let success = true;
+ const appliedDimension = dimension ?? '2d';
+ // Note that we unconditionally test copy usages for all formats. We don't check copySrc/copyDst in kTextureFormatInfo in capability_info.js
+ // if (!info.copySrc && (usage & GPUTextureUsage.COPY_SRC) !== 0) success = false;
+ // if (!info.copyDst && (usage & GPUTextureUsage.COPY_DST) !== 0) success = false;
+ if (!info.storage && (usage & GPUTextureUsage.STORAGE_BINDING) !== 0) success = false;
+ if (
+ (!info.renderable || appliedDimension !== '2d') &&
+ (usage & GPUTextureUsage.RENDER_ATTACHMENT) !== 0
+ )
+ success = false;
+
+ t.expectValidationError(() => {
+ t.device.createTexture(descriptor);
+ }, !success);
+ });
+
+g.test('viewFormats')
+ .desc(
+ `Test creating a texture with viewFormats list for all {texture format}x{view format}. Only compatible view formats should be valid.`
+ )
+ .params(u =>
+ u
+ .combine('formatFeature', kFeaturesForFormats)
+ .combine('viewFormatFeature', kFeaturesForFormats)
+ .beginSubcases()
+ .expand('format', ({ formatFeature }) =>
+ filterFormatsByFeature(formatFeature, kTextureFormats)
+ )
+ .expand('viewFormat', ({ viewFormatFeature }) =>
+ filterFormatsByFeature(viewFormatFeature, kTextureFormats)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { formatFeature, viewFormatFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([formatFeature, viewFormatFeature]);
+ })
+ .fn(async t => {
+ const { format, viewFormat } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+
+ const compatible = viewCompatible(format, viewFormat);
+
+ // Test the viewFormat in the list.
+ t.expectValidationError(() => {
+ t.device.createTexture({
+ format,
+ size: [blockWidth, blockHeight],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ viewFormats: [viewFormat],
+ });
+ }, !compatible);
+
+ // Test the viewFormat and the texture format in the list.
+ t.expectValidationError(() => {
+ t.device.createTexture({
+ format,
+ size: [blockWidth, blockHeight],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ viewFormats: [viewFormat, format],
+ });
+ }, !compatible);
+
+ // Test the viewFormat multiple times in the list.
+ t.expectValidationError(() => {
+ t.device.createTexture({
+ format,
+ size: [blockWidth, blockHeight],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ viewFormats: [viewFormat, viewFormat],
+ });
+ }, !compatible);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts
new file mode 100644
index 0000000000..3f0b02a56f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createView.spec.ts
@@ -0,0 +1,332 @@
+export const description = `createView validation tests.`;
+
+import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { unreachable } from '../../../common/util/util.js';
+import {
+ kTextureAspects,
+ kTextureDimensions,
+ kTextureFormatInfo,
+ kTextureFormats,
+ kTextureViewDimensions,
+ kFeaturesForFormats,
+ viewCompatible,
+ filterFormatsByFeature,
+} from '../../capability_info.js';
+import { kResourceStates } from '../../gpu_test.js';
+import {
+ getTextureDimensionFromView,
+ reifyTextureViewDescriptor,
+ viewDimensionsForTextureDimension,
+} from '../../util/texture/base.js';
+import { reifyExtent3D } from '../../util/unions.js';
+
+import { ValidationTest } from './validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+const kLevels = 6;
+
+g.test('format')
+ .desc(
+ `Views must have the view format compatible with the base texture, for all {texture format}x{view format}.`
+ )
+ .params(u =>
+ u
+ .combine('textureFormatFeature', kFeaturesForFormats)
+ .combine('viewFormatFeature', kFeaturesForFormats)
+ .beginSubcases()
+ .expand('textureFormat', ({ textureFormatFeature }) =>
+ filterFormatsByFeature(textureFormatFeature, kTextureFormats)
+ )
+ .expand('viewFormat', ({ viewFormatFeature }) =>
+ filterFormatsByFeature(viewFormatFeature, [undefined, ...kTextureFormats])
+ )
+ .combine('useViewFormatList', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const { textureFormatFeature, viewFormatFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([textureFormatFeature, viewFormatFeature]);
+ })
+ .fn(async t => {
+ const { textureFormat, viewFormat, useViewFormatList } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[textureFormat];
+
+ const compatible = viewFormat === undefined || viewCompatible(textureFormat, viewFormat);
+
+ const texture = t.device.createTexture({
+ format: textureFormat,
+ size: [blockWidth, blockHeight],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+
+ // This is a test of createView, not createTexture. Don't pass viewFormats here that
+ // are not compatible, as that is tested in createTexture.spec.ts.
+ viewFormats:
+ useViewFormatList && compatible && viewFormat !== undefined ? [viewFormat] : undefined,
+ });
+
+ // Successful if there is no view format, no reinterpretation was required, or the formats are compatible
+ // and is was specified in the viewFormats list.
+ const success =
+ viewFormat === undefined || viewFormat === textureFormat || (compatible && useViewFormatList);
+ t.expectValidationError(() => {
+ texture.createView({ format: viewFormat });
+ }, !success);
+ });
+
+g.test('dimension')
+ .desc(
+ `For all {texture dimension}, {view dimension}, test that they must be compatible:
+ - 1d -> 1d
+ - 2d -> 2d, 2d-array, cube, or cube-array
+ - 3d -> 3d`
+ )
+ .params(u =>
+ u
+ .combine('textureDimension', kTextureDimensions)
+ .combine('viewDimension', [...kTextureViewDimensions, undefined])
+ )
+ .fn(t => {
+ const { textureDimension, viewDimension } = t.params;
+
+ const size = textureDimension === '1d' ? [4] : [4, 4, 6];
+ const textureDescriptor = {
+ format: 'rgba8unorm' as const,
+ dimension: textureDimension,
+ size,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+ const texture = t.device.createTexture(textureDescriptor);
+
+ const view = { dimension: viewDimension };
+ const reified = reifyTextureViewDescriptor(textureDescriptor, view);
+
+ const success = getTextureDimensionFromView(reified.dimension) === textureDimension;
+ t.expectValidationError(() => {
+ texture.createView(view);
+ }, !success);
+ });
+
+g.test('aspect')
+ .desc(
+ `For every {format}x{aspect}, test that the view aspect must exist in the format:
+ - "all" is allowed for any format
+ - "depth-only" is allowed only for depth and depth-stencil formats
+ - "stencil-only" is allowed only for stencil and depth-stencil formats`
+ )
+ .params(u =>
+ u //
+ .combine('format', kTextureFormats)
+ .combine('aspect', kTextureAspects)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const { format, aspect } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const texture = t.device.createTexture({
+ format,
+ size: [info.blockWidth, info.blockHeight, 1],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ const success =
+ aspect === 'all' ||
+ (aspect === 'depth-only' && info.depth) ||
+ (aspect === 'stencil-only' && info.stencil);
+ t.expectValidationError(() => {
+ texture.createView({ aspect });
+ }, !success);
+ });
+
+const kTextureAndViewDimensions = kUnitCaseParamsBuilder
+ .combine('textureDimension', kTextureDimensions)
+ .expand('viewDimension', p => [
+ undefined,
+ ...viewDimensionsForTextureDimension(p.textureDimension),
+ ]);
+
+function validateCreateViewLayersLevels(tex: GPUTextureDescriptor, view: GPUTextureViewDescriptor) {
+ const textureLevels = tex.mipLevelCount ?? 1;
+ const textureLayers = tex.dimension === '2d' ? reifyExtent3D(tex.size).depthOrArrayLayers : 1;
+ const reified = reifyTextureViewDescriptor(tex, view);
+
+ let success =
+ reified.mipLevelCount > 0 &&
+ reified.baseMipLevel < textureLevels &&
+ reified.baseMipLevel + reified.mipLevelCount <= textureLevels &&
+ reified.arrayLayerCount > 0 &&
+ reified.baseArrayLayer < textureLayers &&
+ reified.baseArrayLayer + reified.arrayLayerCount <= textureLayers;
+ if (reified.dimension === '1d' || reified.dimension === '2d' || reified.dimension === '3d') {
+ success &&= reified.arrayLayerCount === 1;
+ } else if (reified.dimension === 'cube') {
+ success &&= reified.arrayLayerCount === 6;
+ } else if (reified.dimension === 'cube-array') {
+ success &&= reified.arrayLayerCount % 6 === 0;
+ }
+ return success;
+}
+
+g.test('array_layers')
+ .desc(
+ `For each texture dimension {1d,2d,3d}, for each possible view dimension for that texture
+ dimension (or undefined, which defaults to the texture dimension), test validation of layer
+ counts:
+ - 1d, 2d, and 3d must have exactly 1 layer
+ - 2d-array must have 1 or more layers
+ - cube must have 6 layers
+ - cube-array must have a positive multiple of 6 layers
+ - Defaulting of baseArrayLayer and arrayLayerCount
+ - baseArrayLayer+arrayLayerCount must be within the texture`
+ )
+ .params(u =>
+ kTextureAndViewDimensions
+ .beginSubcases()
+ .expand('textureLayers', ({ textureDimension: d }) => (d === '2d' ? [1, 6, 18] : [1]))
+ .combine('textureLevels', [1, kLevels])
+ .unless(p => p.textureDimension === '1d' && p.textureLevels !== 1)
+ .expand(
+ 'baseArrayLayer',
+ ({ textureLayers: l }) => new Set([undefined, 0, 1, 5, 6, 7, l - 1, l, l + 1])
+ )
+ .expand('arrayLayerCount', function* ({ textureLayers: l, baseArrayLayer = 0 }) {
+ yield undefined;
+ for (const lastArrayLayer of new Set([0, 1, 5, 6, 7, l - 1, l, l + 1])) {
+ if (baseArrayLayer <= lastArrayLayer) yield lastArrayLayer - baseArrayLayer;
+ }
+ })
+ )
+ .fn(t => {
+ const {
+ textureDimension,
+ viewDimension,
+ textureLayers,
+ textureLevels,
+ baseArrayLayer,
+ arrayLayerCount,
+ } = t.params;
+
+ const kWidth = 1 << (kLevels - 1); // 32
+ const textureDescriptor: GPUTextureDescriptor = {
+ format: 'rgba8unorm',
+ dimension: textureDimension,
+ size:
+ textureDimension === '1d'
+ ? [kWidth]
+ : textureDimension === '2d'
+ ? [kWidth, kWidth, textureLayers]
+ : textureDimension === '3d'
+ ? [kWidth, kWidth, kWidth]
+ : unreachable(),
+ mipLevelCount: textureLevels,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const viewDescriptor = { dimension: viewDimension, baseArrayLayer, arrayLayerCount };
+ const success = validateCreateViewLayersLevels(textureDescriptor, viewDescriptor);
+
+ const texture = t.device.createTexture(textureDescriptor);
+ t.expectValidationError(() => {
+ texture.createView(viewDescriptor);
+ }, !success);
+ });
+
+g.test('mip_levels')
+ .desc(
+ `Views must have at least one level, and must be within the level of the base texture.
+
+ - mipLevelCount=0 at various baseMipLevel values
+ - Cases where baseMipLevel+mipLevelCount goes past the end of the texture
+ - Cases with baseMipLevel or mipLevelCount undefined (compares against reference defaulting impl)
+ `
+ )
+ .params(u =>
+ kTextureAndViewDimensions
+ .beginSubcases()
+ .combine('textureLevels', [1, kLevels - 2, kLevels])
+ .unless(p => p.textureDimension === '1d' && p.textureLevels !== 1)
+ .expand(
+ 'baseMipLevel',
+ ({ textureLevels: l }) => new Set([undefined, 0, 1, 5, 6, 7, l - 1, l, l + 1])
+ )
+ .expand('mipLevelCount', function* ({ textureLevels: l, baseMipLevel = 0 }) {
+ yield undefined;
+ for (const lastMipLevel of new Set([0, 1, 5, 6, 7, l - 1, l, l + 1])) {
+ if (baseMipLevel <= lastMipLevel) yield lastMipLevel - baseMipLevel;
+ }
+ })
+ )
+ .fn(t => {
+ const {
+ textureDimension,
+ viewDimension,
+ textureLevels,
+ baseMipLevel,
+ mipLevelCount,
+ } = t.params;
+
+ const textureDescriptor: GPUTextureDescriptor = {
+ format: 'rgba8unorm',
+ dimension: textureDimension,
+ size:
+ textureDimension === '1d' ? [32] : textureDimension === '3d' ? [32, 32, 32] : [32, 32, 18],
+ mipLevelCount: textureLevels,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ };
+
+ const viewDescriptor = { dimension: viewDimension, baseMipLevel, mipLevelCount };
+ const success = validateCreateViewLayersLevels(textureDescriptor, viewDescriptor);
+
+ const texture = t.device.createTexture(textureDescriptor);
+ t.debug(`${mipLevelCount} ${success}`);
+ t.expectValidationError(() => {
+ texture.createView(viewDescriptor);
+ }, !success);
+ });
+
+g.test('cube_faces_square')
+ .desc(
+ `Test that the X/Y dimensions of cube and cube array textures must be square.
+ - {2d (control case), cube, cube-array}`
+ )
+ .params(u =>
+ u //
+ .combine('dimension', ['2d', 'cube', 'cube-array'] as const)
+ .combine('size', [
+ [4, 4, 6],
+ [5, 5, 6],
+ [4, 5, 6],
+ [4, 8, 6],
+ [8, 4, 6],
+ ])
+ )
+ .fn(async t => {
+ const { dimension, size } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size,
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ const success = dimension === '2d' || size[0] === size[1];
+ t.expectValidationError(() => {
+ texture.createView({ dimension });
+ }, !success);
+ });
+
+g.test('texture_state')
+ .desc(`createView should fail if the texture is invalid (but succeed if it is destroyed)`)
+ .paramsSubcasesOnly(u => u.combine('state', kResourceStates))
+ .fn(async t => {
+ const { state } = t.params;
+ const texture = t.createTextureWithState(state);
+
+ t.expectValidationError(() => {
+ texture.createView();
+ }, state === 'invalid');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/debugMarker.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/debugMarker.spec.ts
new file mode 100644
index 0000000000..2f1f7a2adf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/debugMarker.spec.ts
@@ -0,0 +1,98 @@
+export const description = `
+Test validation of pushDebugGroup, popDebugGroup, and insertDebugMarker.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+import { ValidationTest } from './validation_test.js';
+
+class F extends ValidationTest {
+ beginRenderPass(commandEncoder: GPUCommandEncoder): GPURenderPassEncoder {
+ const attachmentTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ this.trackForCleanup(attachmentTexture);
+ return commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachmentTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('push_pop_call_count_unbalance,command_encoder')
+ .desc(
+ `
+ Test that a validation error is generated if {push,pop} debug group call count is not paired.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('pushCount', [1, 2, 3])
+ .combine('popCount', [1, 2, 3])
+ )
+ .fn(async t => {
+ const { pushCount, popCount } = t.params;
+
+ const encoder = t.device.createCommandEncoder();
+
+ for (let i = 0; i < pushCount; ++i) {
+ encoder.pushDebugGroup('EventStart');
+ }
+
+ encoder.insertDebugMarker('Marker');
+
+ for (let i = 0; i < popCount; ++i) {
+ encoder.popDebugGroup();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, pushCount !== popCount);
+ });
+
+g.test('push_pop_call_count_unbalance,render_compute_pass')
+ .desc(
+ `
+ Test that a validation error is generated if {push,pop} debug group call count is not paired in
+ ComputePassEncoder and RenderPassEncoder.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('passType', ['compute', 'render'])
+ .beginSubcases()
+ .combine('pushCount', [1, 2, 3])
+ .combine('popCount', [1, 2, 3])
+ )
+ .fn(async t => {
+ const { passType, pushCount, popCount } = t.params;
+
+ const encoder = t.device.createCommandEncoder();
+
+ const pass = passType === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder);
+
+ for (let i = 0; i < pushCount; ++i) {
+ pass.pushDebugGroup('EventStart');
+ }
+
+ pass.insertDebugMarker('Marker');
+
+ for (let i = 0; i < popCount; ++i) {
+ pass.popDebugGroup();
+ }
+
+ t.expectValidationError(() => {
+ pass.end();
+ encoder.finish();
+ }, pushCount !== popCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginComputePass.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginComputePass.spec.ts
new file mode 100644
index 0000000000..0b69ba6a91
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginComputePass.spec.ts
@@ -0,0 +1,193 @@
+export const description = `
+Tests for validation in beginComputePass and GPUComputePassDescriptor as its optional descriptor.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kQueryTypes } from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ tryComputePass(success: boolean, descriptor: GPUComputePassDescriptor): void {
+ const encoder = this.device.createCommandEncoder();
+ const computePass = encoder.beginComputePass(descriptor);
+ computePass.end();
+
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('timestampWrites,same_location')
+ .desc(
+ `
+ Test that entries in timestampWrites do not have the same location in GPUComputePassDescriptor.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('locationA', ['beginning', 'end'] as const)
+ .combine('locationB', ['beginning', 'end'] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { locationA, locationB } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: 'timestamp',
+ count: 2,
+ });
+
+ const timestampWriteA = {
+ querySet,
+ queryIndex: 0,
+ location: locationA,
+ };
+
+ const timestampWriteB = {
+ querySet,
+ queryIndex: 1,
+ location: locationB,
+ };
+
+ const isValid = locationA !== locationB;
+
+ const descriptor = {
+ timestampWrites: [timestampWriteA, timestampWriteB],
+ };
+
+ t.tryComputePass(isValid, descriptor);
+ });
+
+g.test('timestampWrites,query_set_type')
+ .desc(
+ `
+ Test that all entries of the timestampWrites must have type 'timestamp'. If all query types are
+ not 'timestamp' in GPUComputePassDescriptor, a validation error should be generated.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('queryTypeA', kQueryTypes)
+ .combine('queryTypeB', kQueryTypes)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForQueryTypeOrSkipTestCase([
+ 'timestamp',
+ t.params.queryTypeA,
+ t.params.queryTypeB,
+ ]);
+ })
+ .fn(async t => {
+ const { queryTypeA, queryTypeB } = t.params;
+
+ const timestampWriteA = {
+ querySet: t.device.createQuerySet({ type: queryTypeA, count: 1 }),
+ queryIndex: 0,
+ location: 'beginning' as const,
+ };
+
+ const timestampWriteB = {
+ querySet: t.device.createQuerySet({ type: queryTypeB, count: 1 }),
+ queryIndex: 0,
+ location: 'end' as const,
+ };
+
+ const isValid = queryTypeA === 'timestamp' && queryTypeB === 'timestamp';
+
+ const descriptor = {
+ timestampWrites: [timestampWriteA, timestampWriteB],
+ };
+
+ t.tryComputePass(isValid, descriptor);
+ });
+
+g.test('timestampWrites,invalid_query_set')
+ .desc(`Tests that timestampWrite that has an invalid query set generates a validation error.`)
+ .params(u => u.combine('querySetState', ['valid', 'invalid'] as const))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { querySetState } = t.params;
+
+ const querySet = t.createQuerySetWithState(querySetState, {
+ type: 'timestamp',
+ count: 1,
+ });
+
+ const timestampWrite = {
+ querySet,
+ queryIndex: 0,
+ location: 'beginning' as const,
+ };
+
+ const descriptor = {
+ timestampWrites: [timestampWrite],
+ };
+
+ t.tryComputePass(querySetState === 'valid', descriptor);
+ });
+
+g.test('timestampWrites,query_index_count')
+ .desc(`Test that querySet.count should be greater than timestampWrite.queryIndex.`)
+ .params(u => u.combine('queryIndex', [0, 1, 2, 3]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { queryIndex } = t.params;
+
+ const querySetCount = 2;
+
+ const timestampWrite = {
+ querySet: t.device.createQuerySet({ type: 'timestamp', count: querySetCount }),
+ queryIndex,
+ location: 'beginning' as const,
+ };
+
+ const isValid = queryIndex < querySetCount;
+
+ const descriptor = {
+ timestampWrites: [timestampWrite],
+ };
+
+ t.tryComputePass(isValid, descriptor);
+ });
+
+g.test('timestamp_query_set,device_mismatch')
+ .desc(
+ `
+ Tests beginComputePass cannot be called with a timestamp query set created from another device.
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ t.selectMismatchedDeviceOrSkipTestCase('timestamp-query');
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const timestampQuerySet = sourceDevice.createQuerySet({
+ type: 'timestamp',
+ count: 1,
+ });
+
+ const timestampWrite = {
+ querySet: timestampQuerySet,
+ queryIndex: 0,
+ location: 'beginning' as const,
+ };
+
+ const descriptor = {
+ timestampWrites: [timestampWrite],
+ };
+
+ t.tryComputePass(!mismatched, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginRenderPass.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginRenderPass.spec.ts
new file mode 100644
index 0000000000..388cdd69c0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/beginRenderPass.spec.ts
@@ -0,0 +1,211 @@
+export const description = `
+Note: render pass 'occlusionQuerySet' validation is tested in queries/general.spec.ts
+
+TODO: Check that depth-stencil attachment views must encompass all aspects.
+
+TODO: check for duplication (render_pass/, etc.), plan, and implement.
+Note possibly a lot of this should be operation tests instead.
+Notes:
+> - color attachments {zero, one, multiple}
+> - many different formats (some are non-renderable)
+> - is a view on a texture with multiple mip levels or array layers
+> - two attachments use the same view, or views of {intersecting, disjoint} ranges
+> - {without, with} resolve target
+> - resolve format compatibility with multisampled format
+> - {all possible load ops, load color {in range, negative, too large}}
+> - all possible store ops
+> - depth/stencil attachment
+> - {unset, all possible formats}
+> - {all possible {depth, stencil} load ops, load values {in range, negative, too large}}
+> - all possible {depth, stencil} store ops
+> - depthReadOnly {t,f}, stencilReadOnly {t,f}
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('color_attachments,device_mismatch')
+ .desc(
+ `
+ Tests beginRenderPass cannot be called with color attachments whose texture view or resolve target is created from another device
+ The 'view' and 'resolveTarget' are:
+ - created from same device in ColorAttachment0 and ColorAttachment1
+ - created from different device in ColorAttachment0 and ColorAttachment1
+ - created from same device in ColorAttachment0, but from different device in ColorAttachment1
+ `
+ )
+ .paramsSubcasesOnly([
+ {
+ view0Mismatched: false,
+ target0Mismatched: false,
+ view1Mismatched: false,
+ target1Mismatched: false,
+ }, // control case
+ {
+ view0Mismatched: false,
+ target0Mismatched: true,
+ view1Mismatched: false,
+ target1Mismatched: true,
+ },
+ {
+ view0Mismatched: true,
+ target0Mismatched: false,
+ view1Mismatched: true,
+ target1Mismatched: false,
+ },
+ {
+ view0Mismatched: false,
+ target0Mismatched: false,
+ view1Mismatched: false,
+ target1Mismatched: true,
+ },
+ ])
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { view0Mismatched, target0Mismatched, view1Mismatched, target1Mismatched } = t.params;
+ const mismatched = view0Mismatched || target0Mismatched || view1Mismatched || target1Mismatched;
+
+ const view0Texture = view0Mismatched
+ ? t.getDeviceMismatchedRenderTexture(4)
+ : t.getRenderTexture(4);
+ const target0Texture = target0Mismatched
+ ? t.getDeviceMismatchedRenderTexture()
+ : t.getRenderTexture();
+ const view1Texture = view1Mismatched
+ ? t.getDeviceMismatchedRenderTexture(4)
+ : t.getRenderTexture(4);
+ const target1Texture = target1Mismatched
+ ? t.getDeviceMismatchedRenderTexture()
+ : t.getRenderTexture();
+
+ const encoder = t.createEncoder('non-pass');
+ const pass = encoder.encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: view0Texture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ resolveTarget: target0Texture.createView(),
+ },
+ {
+ view: view1Texture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ resolveTarget: target1Texture.createView(),
+ },
+ ],
+ });
+ pass.end();
+
+ encoder.validateFinish(!mismatched);
+ });
+
+g.test('depth_stencil_attachment,device_mismatch')
+ .desc(
+ 'Tests beginRenderPass cannot be called with a depth stencil attachment whose texture view is created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+
+ const descriptor: GPUTextureDescriptor = {
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'depth24plus-stencil8',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ };
+
+ const depthStencilTexture = mismatched
+ ? t.getDeviceMismatchedTexture(descriptor)
+ : t.device.createTexture(descriptor);
+
+ const encoder = t.createEncoder('non-pass');
+ const pass = encoder.encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: depthStencilTexture.createView(),
+ depthClearValue: 0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: 0,
+ stencilLoadOp: 'clear',
+ stencilStoreOp: 'store',
+ },
+ });
+ pass.end();
+
+ encoder.validateFinish(!mismatched);
+ });
+
+g.test('occlusion_query_set,device_mismatch')
+ .desc(
+ 'Tests beginRenderPass cannot be called with an occlusion query set created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const occlusionQuerySet = sourceDevice.createQuerySet({
+ type: 'occlusion',
+ count: 1,
+ });
+ t.trackForCleanup(occlusionQuerySet);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ encoder.validateFinish(!mismatched);
+ });
+
+g.test('timestamp_query_set,device_mismatch')
+ .desc(
+ `
+ Tests beginRenderPass cannot be called with a timestamp query set created from another device.
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ t.selectMismatchedDeviceOrSkipTestCase('timestamp-query');
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const timestampWrite = {
+ querySet: sourceDevice.createQuerySet({ type: 'timestamp', count: 1 }),
+ queryIndex: 0,
+ location: 'beginning' as const,
+ };
+
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ const pass = encoder.encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ timestampWrites: [timestampWrite],
+ });
+ pass.end();
+
+ encoder.validateFinish(!mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/clearBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/clearBuffer.spec.ts
new file mode 100644
index 0000000000..7e90db8545
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/clearBuffer.spec.ts
@@ -0,0 +1,246 @@
+export const description = `
+API validation tests for clearBuffer.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { kBufferUsages } from '../../../../capability_info.js';
+import { kResourceStates } from '../../../../gpu_test.js';
+import { kMaxSafeMultipleOf8 } from '../../../../util/math.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ TestClearBuffer(options: {
+ buffer: GPUBuffer;
+ offset: number | undefined;
+ size: number | undefined;
+ isSuccess: boolean;
+ }): void {
+ const { buffer, offset, size, isSuccess } = options;
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.clearBuffer(buffer, offset, size);
+
+ this.expectValidationError(() => {
+ commandEncoder.finish();
+ }, !isSuccess);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('buffer_state')
+ .desc(`Test that clearing an invalid or destroyed buffer fails.`)
+ .params(u => u.combine('bufferState', kResourceStates))
+ .fn(async t => {
+ const { bufferState } = t.params;
+
+ const buffer = t.createBufferWithState(bufferState, {
+ size: 8,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.clearBuffer(buffer, 0, 8);
+
+ if (bufferState === 'invalid') {
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ } else {
+ const cmd = commandEncoder.finish();
+ t.expectValidationError(() => {
+ t.device.queue.submit([cmd]);
+ }, bufferState === 'destroyed');
+ }
+ });
+
+g.test('buffer,device_mismatch')
+ .desc(`Tests clearBuffer cannot be called with buffer created from another device.`)
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+ const size = 8;
+
+ const buffer = sourceDevice.createBuffer({
+ size,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(buffer);
+
+ t.TestClearBuffer({
+ buffer,
+ offset: 0,
+ size,
+ isSuccess: !mismatched,
+ });
+ });
+
+g.test('default_args')
+ .desc(`Test that calling clearBuffer with a default offset and size is valid.`)
+ .paramsSubcasesOnly([
+ { offset: undefined, size: undefined },
+ { offset: 4, size: undefined },
+ { offset: undefined, size: 8 },
+ ] as const)
+ .fn(async t => {
+ const { offset, size } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset,
+ size,
+ isSuccess: true,
+ });
+ });
+
+g.test('buffer_usage')
+ .desc(`Test that only buffers with COPY_DST usage are valid to use with copyBuffers.`)
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('usage', kBufferUsages)
+ )
+ .fn(async t => {
+ const { usage } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset: 0,
+ size: 16,
+ isSuccess: usage === GPUBufferUsage.COPY_DST,
+ });
+ });
+
+g.test('size_alignment')
+ .desc(
+ `
+ Test that the clear size must be 4 byte aligned.
+ - Test size is not a multiple of 4.
+ - Test size is 0.
+ - Test size overflows the buffer size.
+ - Test size is omitted.
+ `
+ )
+ .paramsSubcasesOnly([
+ { size: 0, _isSuccess: true },
+ { size: 2, _isSuccess: false },
+ { size: 4, _isSuccess: true },
+ { size: 5, _isSuccess: false },
+ { size: 8, _isSuccess: true },
+ { size: 20, _isSuccess: false },
+ { size: undefined, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { size, _isSuccess: isSuccess } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset: 0,
+ size,
+ isSuccess,
+ });
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+ Test that the clear offsets must be 4 byte aligned.
+ - Test offset is not a multiple of 4.
+ - Test offset is larger than the buffer size.
+ - Test offset is omitted.
+ `
+ )
+ .paramsSubcasesOnly([
+ { offset: 0, _isSuccess: true },
+ { offset: 2, _isSuccess: false },
+ { offset: 4, _isSuccess: true },
+ { offset: 5, _isSuccess: false },
+ { offset: 8, _isSuccess: true },
+ { offset: 20, _isSuccess: false },
+ { offset: undefined, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { offset, _isSuccess: isSuccess } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset,
+ size: 8,
+ isSuccess,
+ });
+ });
+
+g.test('overflow')
+ .desc(`Test that clears which may cause arthimetic overflows are invalid.`)
+ .paramsSubcasesOnly([
+ { offset: 0, size: kMaxSafeMultipleOf8 },
+ { offset: 16, size: kMaxSafeMultipleOf8 },
+ { offset: kMaxSafeMultipleOf8, size: 16 },
+ { offset: kMaxSafeMultipleOf8, size: kMaxSafeMultipleOf8 },
+ ] as const)
+ .fn(async t => {
+ const { offset, size } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset,
+ size,
+ isSuccess: false,
+ });
+ });
+
+g.test('out_of_bounds')
+ .desc(`Test that clears which exceed the buffer bounds are invalid.`)
+ .paramsSubcasesOnly([
+ { offset: 0, size: 32, _isSuccess: true },
+ { offset: 0, size: 36 },
+ { offset: 32, size: 0, _isSuccess: true },
+ { offset: 32, size: 4 },
+ { offset: 36, size: 4 },
+ { offset: 36, size: 0 },
+ { offset: 20, size: 16 },
+ { offset: 20, size: 12, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { offset, size, _isSuccess = false } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 32,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestClearBuffer({
+ buffer,
+ offset,
+ size,
+ isSuccess: _isSuccess,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/compute_pass.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/compute_pass.spec.ts
new file mode 100644
index 0000000000..0a90793224
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/compute_pass.spec.ts
@@ -0,0 +1,250 @@
+export const description = `
+API validation test for compute pass
+
+Does **not** test usage scopes (resource_usages/) or programmable pass stuff (programmable_pass).
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { kBufferUsages, kLimitInfo } from '../../../../capability_info.js';
+import { GPUConst } from '../../../../constants.js';
+import { kResourceStates, ResourceState } from '../../../../gpu_test.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ createComputePipeline(state: 'valid' | 'invalid'): GPUComputePipeline {
+ if (state === 'valid') {
+ return this.createNoOpComputePipeline();
+ }
+
+ return this.createErrorComputePipeline();
+ }
+
+ createIndirectBuffer(state: ResourceState, data: Uint32Array): GPUBuffer {
+ const descriptor: GPUBufferDescriptor = {
+ size: data.byteLength,
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
+ };
+
+ if (state === 'invalid') {
+ descriptor.usage = 0xffff; // Invalid GPUBufferUsage
+ }
+
+ this.device.pushErrorScope('validation');
+ const buffer = this.device.createBuffer(descriptor);
+ void this.device.popErrorScope();
+
+ if (state === 'valid') {
+ this.queue.writeBuffer(buffer, 0, data);
+ }
+
+ if (state === 'destroyed') {
+ buffer.destroy();
+ }
+
+ return buffer;
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('set_pipeline')
+ .desc(
+ `
+setPipeline should generate an error iff using an 'invalid' pipeline.
+`
+ )
+ .params(u => u.beginSubcases().combine('state', ['valid', 'invalid'] as const))
+ .fn(t => {
+ const { state } = t.params;
+ const pipeline = t.createComputePipeline(state);
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('pipeline,device_mismatch')
+ .desc('Tests setPipeline cannot be called with a compute pipeline created from another device')
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const pipeline = sourceDevice.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: sourceDevice.createShaderModule({
+ code: '@compute @workgroup_size(1) fn main() {}',
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const { encoder, validateFinish } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+ validateFinish(!mismatched);
+ });
+
+const kMaxDispatch = kLimitInfo.maxComputeWorkgroupsPerDimension.default;
+g.test('dispatch_sizes')
+ .desc(
+ `Test 'direct' and 'indirect' dispatch with various sizes.
+
+ Only direct dispatches can produce validation errors.
+ Workgroup sizes:
+ - valid: { zero, one, just under limit }
+ - invalid: { just over limit, way over limit }
+
+ TODO: Verify that the invalid cases don't execute any invocations at all.
+`
+ )
+ .params(u =>
+ u
+ .combine('dispatchType', ['direct', 'indirect'] as const)
+ .combine('largeDimValue', [0, 1, kMaxDispatch, kMaxDispatch + 1, 0x7fff_ffff, 0xffff_ffff])
+ .beginSubcases()
+ .combine('largeDimIndex', [0, 1, 2] as const)
+ .combine('smallDimValue', [0, 1])
+ )
+ .fn(t => {
+ const { dispatchType, largeDimIndex, smallDimValue, largeDimValue } = t.params;
+
+ const pipeline = t.createNoOpComputePipeline();
+
+ const workSizes = [smallDimValue, smallDimValue, smallDimValue];
+ workSizes[largeDimIndex] = largeDimValue;
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+ if (dispatchType === 'direct') {
+ const [x, y, z] = workSizes;
+ encoder.dispatchWorkgroups(x, y, z);
+ } else if (dispatchType === 'indirect') {
+ encoder.dispatchWorkgroupsIndirect(
+ t.createIndirectBuffer('valid', new Uint32Array(workSizes)),
+ 0
+ );
+ }
+
+ const shouldError =
+ dispatchType === 'direct' &&
+ (workSizes[0] > kMaxDispatch || workSizes[1] > kMaxDispatch || workSizes[2] > kMaxDispatch);
+
+ validateFinishAndSubmit(!shouldError, true);
+ });
+
+const kBufferData = new Uint32Array(6).fill(1);
+g.test('indirect_dispatch_buffer_state')
+ .desc(
+ `
+Test dispatchWorkgroupsIndirect validation by submitting various dispatches with a no-op pipeline
+and an indirectBuffer with 6 elements.
+- indirectBuffer: {'valid', 'invalid', 'destroyed'}
+- indirectOffset:
+ - valid, within the buffer: {beginning, middle, end} of the buffer
+ - invalid, non-multiple of 4
+ - invalid, the last element is outside the buffer
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('state', kResourceStates)
+ .combine('offset', [
+ // valid (for 'valid' buffers)
+ 0,
+ Uint32Array.BYTES_PER_ELEMENT,
+ kBufferData.byteLength - 3 * Uint32Array.BYTES_PER_ELEMENT,
+ // invalid, non-multiple of 4 offset
+ 1,
+ // invalid, last element outside buffer
+ kBufferData.byteLength - 2 * Uint32Array.BYTES_PER_ELEMENT,
+ ])
+ )
+ .fn(t => {
+ const { state, offset } = t.params;
+ const pipeline = t.createNoOpComputePipeline();
+ const buffer = t.createIndirectBuffer(state, kBufferData);
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+ encoder.dispatchWorkgroupsIndirect(buffer, offset);
+
+ const finishShouldError =
+ state === 'invalid' ||
+ offset % 4 !== 0 ||
+ offset + 3 * Uint32Array.BYTES_PER_ELEMENT > kBufferData.byteLength;
+ validateFinishAndSubmit(!finishShouldError, state !== 'destroyed');
+ });
+
+g.test('indirect_dispatch_buffer,device_mismatch')
+ .desc(
+ `Tests dispatchWorkgroupsIndirect cannot be called with an indirect buffer created from another device`
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+
+ const pipeline = t.createNoOpComputePipeline();
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const buffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+ t.trackForCleanup(buffer);
+
+ const { encoder, validateFinish } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+ encoder.dispatchWorkgroupsIndirect(buffer, 0);
+ validateFinish(!mismatched);
+ });
+
+g.test('indirect_dispatch_buffer,usage')
+ .desc(
+ `
+ Tests dispatchWorkgroupsIndirect generates a validation error if the buffer usage does not
+ contain INDIRECT usage.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ // If bufferUsage0 and bufferUsage1 are the same, the usage being test is a single usage.
+ // Otherwise, it's a combined usage.
+ .combine('bufferUsage0', kBufferUsages)
+ .combine('bufferUsage1', kBufferUsages)
+ .unless(
+ ({ bufferUsage0, bufferUsage1 }) =>
+ ((bufferUsage0 | bufferUsage1) &
+ (GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE)) !==
+ 0
+ )
+ )
+ .fn(async t => {
+ const { bufferUsage0, bufferUsage1 } = t.params;
+
+ const bufferUsage = bufferUsage0 | bufferUsage1;
+
+ const layout = t.device.createPipelineLayout({ bindGroupLayouts: [] });
+ const pipeline = t.createNoOpComputePipeline(layout);
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: bufferUsage,
+ });
+ t.trackForCleanup(buffer);
+
+ const success = (GPUBufferUsage.INDIRECT & bufferUsage) !== 0;
+
+ const { encoder, validateFinish } = t.createEncoder('compute pass');
+ encoder.setPipeline(pipeline);
+
+ encoder.dispatchWorkgroupsIndirect(buffer, 0);
+ validateFinish(success);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyBufferToBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyBufferToBuffer.spec.ts
new file mode 100644
index 0000000000..918bebf7d7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyBufferToBuffer.spec.ts
@@ -0,0 +1,326 @@
+export const description = `
+copyBufferToBuffer tests.
+
+Test Plan:
+* Buffer is valid/invalid
+ - the source buffer is invalid
+ - the destination buffer is invalid
+* Buffer usages
+ - the source buffer is created without GPUBufferUsage::COPY_SRC
+ - the destination buffer is created without GPUBufferUsage::COPY_DEST
+* CopySize
+ - copySize is not a multiple of 4
+ - copySize is 0
+* copy offsets
+ - sourceOffset is not a multiple of 4
+ - destinationOffset is not a multiple of 4
+* Arithmetic overflow
+ - (sourceOffset + copySize) is overflow
+ - (destinationOffset + copySize) is overflow
+* Out of bounds
+ - (sourceOffset + copySize) > size of source buffer
+ - (destinationOffset + copySize) > size of destination buffer
+* Source buffer and destination buffer are the same buffer
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { kBufferUsages } from '../../../../capability_info.js';
+import { kResourceStates } from '../../../../gpu_test.js';
+import { kMaxSafeMultipleOf8 } from '../../../../util/math.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ TestCopyBufferToBuffer(options: {
+ srcBuffer: GPUBuffer;
+ srcOffset: number;
+ dstBuffer: GPUBuffer;
+ dstOffset: number;
+ copySize: number;
+ expectation: 'Success' | 'FinishError' | 'SubmitError';
+ }): void {
+ const { srcBuffer, srcOffset, dstBuffer, dstOffset, copySize, expectation } = options;
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyBufferToBuffer(srcBuffer, srcOffset, dstBuffer, dstOffset, copySize);
+
+ if (expectation === 'FinishError') {
+ this.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ } else {
+ const cmd = commandEncoder.finish();
+ this.expectValidationError(() => {
+ this.device.queue.submit([cmd]);
+ }, expectation === 'SubmitError');
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('buffer_state')
+ .params(u =>
+ u //
+ .combine('srcBufferState', kResourceStates)
+ .combine('dstBufferState', kResourceStates)
+ )
+ .fn(async t => {
+ const { srcBufferState, dstBufferState } = t.params;
+ const srcBuffer = t.createBufferWithState(srcBufferState, {
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ const dstBuffer = t.createBufferWithState(dstBufferState, {
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const shouldFinishError = srcBufferState === 'invalid' || dstBufferState === 'invalid';
+ const shouldSubmitSuccess = srcBufferState === 'valid' && dstBufferState === 'valid';
+ const expectation = shouldSubmitSuccess
+ ? 'Success'
+ : shouldFinishError
+ ? 'FinishError'
+ : 'SubmitError';
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset: 0,
+ dstBuffer,
+ dstOffset: 0,
+ copySize: 8,
+ expectation,
+ });
+ });
+
+g.test('buffer,device_mismatch')
+ .desc(
+ 'Tests copyBufferToBuffer cannot be called with src buffer or dst buffer created from another device'
+ )
+ .paramsSubcasesOnly([
+ { srcMismatched: false, dstMismatched: false }, // control case
+ { srcMismatched: true, dstMismatched: false },
+ { srcMismatched: false, dstMismatched: true },
+ ] as const)
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { srcMismatched, dstMismatched } = t.params;
+
+ const srcBufferDevice = srcMismatched ? t.mismatchedDevice : t.device;
+ const srcBuffer = srcBufferDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(srcBuffer);
+
+ const dstBufferDevice = dstMismatched ? t.mismatchedDevice : t.device;
+ const dstBuffer = dstBufferDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(dstBuffer);
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset: 0,
+ dstBuffer,
+ dstOffset: 0,
+ copySize: 8,
+ expectation: srcMismatched || dstMismatched ? 'FinishError' : 'Success',
+ });
+ });
+
+g.test('buffer_usage')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcUsage', kBufferUsages)
+ .combine('dstUsage', kBufferUsages)
+ )
+ .fn(async t => {
+ const { srcUsage, dstUsage } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 16,
+ usage: srcUsage,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 16,
+ usage: dstUsage,
+ });
+
+ const isSuccess = srcUsage === GPUBufferUsage.COPY_SRC && dstUsage === GPUBufferUsage.COPY_DST;
+ const expectation = isSuccess ? 'Success' : 'FinishError';
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset: 0,
+ dstBuffer,
+ dstOffset: 0,
+ copySize: 8,
+ expectation,
+ });
+ });
+
+g.test('copy_size_alignment')
+ .paramsSubcasesOnly([
+ { copySize: 0, _isSuccess: true },
+ { copySize: 2, _isSuccess: false },
+ { copySize: 4, _isSuccess: true },
+ { copySize: 5, _isSuccess: false },
+ { copySize: 8, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { copySize, _isSuccess: isSuccess } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset: 0,
+ dstBuffer,
+ dstOffset: 0,
+ copySize,
+ expectation: isSuccess ? 'Success' : 'FinishError',
+ });
+ });
+
+g.test('copy_offset_alignment')
+ .paramsSubcasesOnly([
+ { srcOffset: 0, dstOffset: 0, _isSuccess: true },
+ { srcOffset: 2, dstOffset: 0, _isSuccess: false },
+ { srcOffset: 4, dstOffset: 0, _isSuccess: true },
+ { srcOffset: 5, dstOffset: 0, _isSuccess: false },
+ { srcOffset: 8, dstOffset: 0, _isSuccess: true },
+ { srcOffset: 0, dstOffset: 2, _isSuccess: false },
+ { srcOffset: 0, dstOffset: 4, _isSuccess: true },
+ { srcOffset: 0, dstOffset: 5, _isSuccess: false },
+ { srcOffset: 0, dstOffset: 8, _isSuccess: true },
+ { srcOffset: 4, dstOffset: 4, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { srcOffset, dstOffset, _isSuccess: isSuccess } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset,
+ dstBuffer,
+ dstOffset,
+ copySize: 8,
+ expectation: isSuccess ? 'Success' : 'FinishError',
+ });
+ });
+
+g.test('copy_overflow')
+ .paramsSubcasesOnly([
+ { srcOffset: 0, dstOffset: 0, copySize: kMaxSafeMultipleOf8 },
+ { srcOffset: 16, dstOffset: 0, copySize: kMaxSafeMultipleOf8 },
+ { srcOffset: 0, dstOffset: 16, copySize: kMaxSafeMultipleOf8 },
+ { srcOffset: kMaxSafeMultipleOf8, dstOffset: 0, copySize: 16 },
+ { srcOffset: 0, dstOffset: kMaxSafeMultipleOf8, copySize: 16 },
+ { srcOffset: kMaxSafeMultipleOf8, dstOffset: 0, copySize: kMaxSafeMultipleOf8 },
+ { srcOffset: 0, dstOffset: kMaxSafeMultipleOf8, copySize: kMaxSafeMultipleOf8 },
+ {
+ srcOffset: kMaxSafeMultipleOf8,
+ dstOffset: kMaxSafeMultipleOf8,
+ copySize: kMaxSafeMultipleOf8,
+ },
+ ] as const)
+ .fn(async t => {
+ const { srcOffset, dstOffset, copySize } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset,
+ dstBuffer,
+ dstOffset,
+ copySize,
+ expectation: 'FinishError',
+ });
+ });
+
+g.test('copy_out_of_bounds')
+ .paramsSubcasesOnly([
+ { srcOffset: 0, dstOffset: 0, copySize: 32, _isSuccess: true },
+ { srcOffset: 0, dstOffset: 0, copySize: 36 },
+ { srcOffset: 36, dstOffset: 0, copySize: 4 },
+ { srcOffset: 0, dstOffset: 36, copySize: 4 },
+ { srcOffset: 36, dstOffset: 0, copySize: 0 },
+ { srcOffset: 0, dstOffset: 36, copySize: 0 },
+ { srcOffset: 20, dstOffset: 0, copySize: 16 },
+ { srcOffset: 20, dstOffset: 0, copySize: 12, _isSuccess: true },
+ { srcOffset: 0, dstOffset: 20, copySize: 16 },
+ { srcOffset: 0, dstOffset: 20, copySize: 12, _isSuccess: true },
+ ] as const)
+ .fn(async t => {
+ const { srcOffset, dstOffset, copySize, _isSuccess = false } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 32,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 32,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer,
+ srcOffset,
+ dstBuffer,
+ dstOffset,
+ copySize,
+ expectation: _isSuccess ? 'Success' : 'FinishError',
+ });
+ });
+
+g.test('copy_within_same_buffer')
+ .paramsSubcasesOnly([
+ { srcOffset: 0, dstOffset: 8, copySize: 4 },
+ { srcOffset: 8, dstOffset: 0, copySize: 4 },
+ { srcOffset: 0, dstOffset: 4, copySize: 8 },
+ { srcOffset: 4, dstOffset: 0, copySize: 8 },
+ ] as const)
+ .fn(async t => {
+ const { srcOffset, dstOffset, copySize } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ t.TestCopyBufferToBuffer({
+ srcBuffer: buffer,
+ srcOffset,
+ dstBuffer: buffer,
+ dstOffset,
+ copySize,
+ expectation: 'FinishError',
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts
new file mode 100644
index 0000000000..9d01055e6d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/copyTextureToTexture.spec.ts
@@ -0,0 +1,876 @@
+export const description = `
+copyTextureToTexture tests.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import {
+ kTextureFormatInfo,
+ kTextureFormats,
+ kCompressedTextureFormats,
+ kDepthStencilFormats,
+ kTextureUsages,
+ textureDimensionAndFormatCompatible,
+ kTextureDimensions,
+ kFeaturesForFormats,
+ filterFormatsByFeature,
+} from '../../../../capability_info.js';
+import { kResourceStates } from '../../../../gpu_test.js';
+import { align, lcm } from '../../../../util/math.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ TestCopyTextureToTexture(
+ source: GPUImageCopyTexture,
+ destination: GPUImageCopyTexture,
+ copySize: GPUExtent3D,
+ expectation: 'Success' | 'FinishError' | 'SubmitError'
+ ): void {
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyTextureToTexture(source, destination, copySize);
+
+ if (expectation === 'FinishError') {
+ this.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ } else {
+ const cmd = commandEncoder.finish();
+ this.expectValidationError(() => {
+ this.device.queue.submit([cmd]);
+ }, expectation === 'SubmitError');
+ }
+ }
+
+ GetPhysicalSubresourceSize(
+ dimension: GPUTextureDimension,
+ textureSize: Required<GPUExtent3DDict>,
+ format: GPUTextureFormat,
+ mipLevel: number
+ ): Required<GPUExtent3DDict> {
+ const virtualWidthAtLevel = Math.max(textureSize.width >> mipLevel, 1);
+ const virtualHeightAtLevel = Math.max(textureSize.height >> mipLevel, 1);
+ const physicalWidthAtLevel = align(virtualWidthAtLevel, kTextureFormatInfo[format].blockWidth);
+ const physicalHeightAtLevel = align(
+ virtualHeightAtLevel,
+ kTextureFormatInfo[format].blockHeight
+ );
+
+ switch (dimension) {
+ case '1d':
+ return { width: physicalWidthAtLevel, height: 1, depthOrArrayLayers: 1 };
+ case '2d':
+ return {
+ width: physicalWidthAtLevel,
+ height: physicalHeightAtLevel,
+ depthOrArrayLayers: textureSize.depthOrArrayLayers,
+ };
+ case '3d':
+ return {
+ width: physicalWidthAtLevel,
+ height: physicalHeightAtLevel,
+ depthOrArrayLayers: Math.max(textureSize.depthOrArrayLayers >> mipLevel, 1),
+ };
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('copy_with_invalid_or_destroyed_texture')
+ .desc('Test copyTextureToTexture is an error when one of the textures is invalid or destroyed.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcState', kResourceStates)
+ .combine('dstState', kResourceStates)
+ )
+ .fn(async t => {
+ const { srcState, dstState } = t.params;
+
+ const textureDesc: GPUTextureDescriptor = {
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ };
+
+ const srcTexture = t.createTextureWithState(srcState, textureDesc);
+ const dstTexture = t.createTextureWithState(dstState, textureDesc);
+
+ const isSubmitSuccess = srcState === 'valid' && dstState === 'valid';
+ const isFinishSuccess = srcState !== 'invalid' && dstState !== 'invalid';
+ const expectation = isFinishSuccess
+ ? isSubmitSuccess
+ ? 'Success'
+ : 'SubmitError'
+ : 'FinishError';
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ expectation
+ );
+ });
+
+g.test('texture,device_mismatch')
+ .desc(
+ 'Tests copyTextureToTexture cannot be called with src texture or dst texture created from another device.'
+ )
+ .paramsSubcasesOnly([
+ { srcMismatched: false, dstMismatched: false }, // control case
+ { srcMismatched: true, dstMismatched: false },
+ { srcMismatched: false, dstMismatched: true },
+ ] as const)
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { srcMismatched, dstMismatched } = t.params;
+
+ const size = { width: 4, height: 4, depthOrArrayLayers: 1 };
+ const format = 'rgba8unorm';
+
+ const srcTextureDevice = srcMismatched ? t.mismatchedDevice : t.device;
+ const srcTexture = srcTextureDevice.createTexture({
+ size,
+ format,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ t.trackForCleanup(srcTexture);
+
+ const dstTextureDevice = dstMismatched ? t.mismatchedDevice : t.device;
+ const dstTexture = dstTextureDevice.createTexture({
+ size,
+ format,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+ t.trackForCleanup(dstTexture);
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ srcMismatched || dstMismatched ? 'FinishError' : 'Success'
+ );
+ });
+
+g.test('mipmap_level')
+ .desc(
+ `
+Test copyTextureToTexture must specify mipLevels that are in range.
+- for various dimensions
+- for various mip level count in the texture
+- for various copy target mip level (in range and not in range)
+`
+ )
+ .params(u =>
+ u //
+ .combine('dimension', kTextureDimensions)
+ .beginSubcases()
+ .combineWithParams([
+ { srcLevelCount: 1, dstLevelCount: 1, srcCopyLevel: 0, dstCopyLevel: 0 },
+ { srcLevelCount: 1, dstLevelCount: 1, srcCopyLevel: 1, dstCopyLevel: 0 },
+ { srcLevelCount: 1, dstLevelCount: 1, srcCopyLevel: 0, dstCopyLevel: 1 },
+ { srcLevelCount: 3, dstLevelCount: 3, srcCopyLevel: 0, dstCopyLevel: 0 },
+ { srcLevelCount: 3, dstLevelCount: 3, srcCopyLevel: 2, dstCopyLevel: 0 },
+ { srcLevelCount: 3, dstLevelCount: 3, srcCopyLevel: 3, dstCopyLevel: 0 },
+ { srcLevelCount: 3, dstLevelCount: 3, srcCopyLevel: 0, dstCopyLevel: 2 },
+ { srcLevelCount: 3, dstLevelCount: 3, srcCopyLevel: 0, dstCopyLevel: 3 },
+ ] as const)
+ .unless(p => p.dimension === '1d' && (p.srcLevelCount !== 1 || p.dstLevelCount !== 1))
+ )
+
+ .fn(async t => {
+ const { srcLevelCount, dstLevelCount, srcCopyLevel, dstCopyLevel, dimension } = t.params;
+
+ const srcTexture = t.device.createTexture({
+ size: { width: 32, height: 1, depthOrArrayLayers: 1 },
+ dimension,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC,
+ mipLevelCount: srcLevelCount,
+ });
+ const dstTexture = t.device.createTexture({
+ size: { width: 32, height: 1, depthOrArrayLayers: 1 },
+ dimension,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_DST,
+ mipLevelCount: dstLevelCount,
+ });
+
+ const isSuccess = srcCopyLevel < srcLevelCount && dstCopyLevel < dstLevelCount;
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, mipLevel: srcCopyLevel },
+ { texture: dstTexture, mipLevel: dstCopyLevel },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('texture_usage')
+ .desc(
+ `
+Test that copyTextureToTexture source/destination need COPY_SRC/COPY_DST usages.
+- for all possible source texture usages
+- for all possible destination texture usages
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcUsage', kTextureUsages)
+ .combine('dstUsage', kTextureUsages)
+ )
+ .fn(async t => {
+ const { srcUsage, dstUsage } = t.params;
+
+ const srcTexture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: srcUsage,
+ });
+ const dstTexture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: dstUsage,
+ });
+
+ const isSuccess =
+ srcUsage === GPUTextureUsage.COPY_SRC && dstUsage === GPUTextureUsage.COPY_DST;
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('sample_count')
+ .desc(
+ `
+Test that textures in copyTextureToTexture must have the same sample count.
+- for various source texture sample count
+- for various destination texture sample count
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcSampleCount', [1, 4])
+ .combine('dstSampleCount', [1, 4])
+ )
+ .fn(async t => {
+ const { srcSampleCount, dstSampleCount } = t.params;
+
+ const srcTexture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: srcSampleCount,
+ });
+ const dstTexture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: dstSampleCount,
+ });
+
+ const isSuccess = srcSampleCount === dstSampleCount;
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ { width: 4, height: 4, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('multisampled_copy_restrictions')
+ .desc(
+ `
+Test that copyTextureToTexture of multisampled texture must copy a whole subresource to a whole subresource.
+- for various origin for the source and destination of the copies.
+
+Note: this is only tested for 2D textures as it is the only dimension compatible with multisampling.
+TODO: Check the source and destination constraints separately.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcCopyOrigin', [
+ { x: 0, y: 0, z: 0 },
+ { x: 1, y: 0, z: 0 },
+ { x: 0, y: 1, z: 0 },
+ { x: 1, y: 1, z: 0 },
+ ])
+ .combine('dstCopyOrigin', [
+ { x: 0, y: 0, z: 0 },
+ { x: 1, y: 0, z: 0 },
+ { x: 0, y: 1, z: 0 },
+ { x: 1, y: 1, z: 0 },
+ ])
+ .expand('copyWidth', p => [32 - Math.max(p.srcCopyOrigin.x, p.dstCopyOrigin.x), 16])
+ .expand('copyHeight', p => [16 - Math.max(p.srcCopyOrigin.y, p.dstCopyOrigin.y), 8])
+ )
+ .fn(async t => {
+ const { srcCopyOrigin, dstCopyOrigin, copyWidth, copyHeight } = t.params;
+
+ const kWidth = 32;
+ const kHeight = 16;
+
+ // Currently we don't support multisampled 2D array textures and the mipmap level count of the
+ // multisampled textures must be 1.
+ const srcTexture = t.device.createTexture({
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: 4,
+ });
+ const dstTexture = t.device.createTexture({
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: 4,
+ });
+
+ const isSuccess = copyWidth === kWidth && copyHeight === kHeight;
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: srcCopyOrigin },
+ { texture: dstTexture, origin: dstCopyOrigin },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('texture_format_compatibility')
+ .desc(
+ `
+Test the formats of textures in copyTextureToTexture must be copy-compatible.
+- for all source texture formats
+- for all destination texture formats
+`
+ )
+ .params(u =>
+ u
+ .combine('srcFormatFeature', kFeaturesForFormats)
+ .combine('dstFormatFeature', kFeaturesForFormats)
+ .beginSubcases()
+ .expand('srcFormat', ({ srcFormatFeature }) =>
+ filterFormatsByFeature(srcFormatFeature, kTextureFormats)
+ )
+ .expand('dstFormat', ({ dstFormatFeature }) =>
+ filterFormatsByFeature(dstFormatFeature, kTextureFormats)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { srcFormatFeature, dstFormatFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([srcFormatFeature, dstFormatFeature]);
+ })
+ .fn(async t => {
+ const { srcFormat, dstFormat } = t.params;
+ const srcFormatInfo = kTextureFormatInfo[srcFormat];
+ const dstFormatInfo = kTextureFormatInfo[dstFormat];
+
+ const textureSize = {
+ width: lcm(srcFormatInfo.blockWidth, dstFormatInfo.blockWidth),
+ height: lcm(srcFormatInfo.blockHeight, dstFormatInfo.blockHeight),
+ depthOrArrayLayers: 1,
+ };
+
+ const srcTexture = t.device.createTexture({
+ size: textureSize,
+ format: srcFormat,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+
+ const dstTexture = t.device.createTexture({
+ size: textureSize,
+ format: dstFormat,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ // Allow copy between compatible format textures.
+ const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat ?? srcFormat;
+ const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat ?? dstFormat;
+ const isSuccess = srcBaseFormat === dstBaseFormat;
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ textureSize,
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('depth_stencil_copy_restrictions')
+ .desc(
+ `
+Test that depth textures subresources must be entirely copied in copyTextureToTexture
+- for various depth-stencil formats
+- for various copy origin and size offsets
+- for various source and destination texture sizes
+- for various source and destination mip levels
+
+Note: this is only tested for 2D textures as it is the only dimension compatible with depth-stencil.
+`
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('copyBoxOffsets', [
+ { x: 0, y: 0, width: 0, height: 0 },
+ { x: 1, y: 0, width: 0, height: 0 },
+ { x: 0, y: 1, width: 0, height: 0 },
+ { x: 0, y: 0, width: -1, height: 0 },
+ { x: 0, y: 0, width: 0, height: -1 },
+ ])
+ .combine('srcTextureSize', [
+ { width: 64, height: 64, depthOrArrayLayers: 1 },
+ { width: 64, height: 32, depthOrArrayLayers: 1 },
+ { width: 32, height: 32, depthOrArrayLayers: 1 },
+ ])
+ .combine('dstTextureSize', [
+ { width: 64, height: 64, depthOrArrayLayers: 1 },
+ { width: 64, height: 32, depthOrArrayLayers: 1 },
+ { width: 32, height: 32, depthOrArrayLayers: 1 },
+ ])
+ .combine('srcCopyLevel', [1, 2])
+ .combine('dstCopyLevel', [0, 1])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const {
+ format,
+ copyBoxOffsets,
+ srcTextureSize,
+ dstTextureSize,
+ srcCopyLevel,
+ dstCopyLevel,
+ } = t.params;
+ const kMipLevelCount = 3;
+
+ const srcTexture = t.device.createTexture({
+ size: { width: srcTextureSize.width, height: srcTextureSize.height, depthOrArrayLayers: 1 },
+ format,
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ const dstTexture = t.device.createTexture({
+ size: { width: dstTextureSize.width, height: dstTextureSize.height, depthOrArrayLayers: 1 },
+ format,
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ const srcSizeAtLevel = t.GetPhysicalSubresourceSize('2d', srcTextureSize, format, srcCopyLevel);
+ const dstSizeAtLevel = t.GetPhysicalSubresourceSize('2d', dstTextureSize, format, dstCopyLevel);
+
+ const copyOrigin = { x: copyBoxOffsets.x, y: copyBoxOffsets.y, z: 0 };
+
+ const copyWidth =
+ Math.min(srcSizeAtLevel.width, dstSizeAtLevel.width) + copyBoxOffsets.width - copyOrigin.x;
+ const copyHeight =
+ Math.min(srcSizeAtLevel.height, dstSizeAtLevel.height) + copyBoxOffsets.height - copyOrigin.y;
+
+ // Depth/stencil copies must copy whole subresources.
+ const isSuccess =
+ copyOrigin.x === 0 &&
+ copyOrigin.y === 0 &&
+ copyWidth === srcSizeAtLevel.width &&
+ copyHeight === srcSizeAtLevel.height &&
+ copyWidth === dstSizeAtLevel.width &&
+ copyHeight === dstSizeAtLevel.height;
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: copyOrigin, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: copyOrigin, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: 1 },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('copy_ranges')
+ .desc(
+ `
+Test that copyTextureToTexture copy boxes must be in range of the subresource.
+- for various dimensions
+- for various offsets to a full copy for the copy origin/size
+- for various copy mip levels
+`
+ )
+ .params(u =>
+ u
+ .combine('dimension', kTextureDimensions)
+ //.beginSubcases()
+ .combine('copyBoxOffsets', [
+ { x: 0, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 1, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 1, y: 0, z: 0, width: -1, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 1, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 1, z: 0, width: 0, height: -1, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 1, width: 0, height: 1, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 2, width: 0, height: 1, depthOrArrayLayers: 0 },
+ { x: 0, y: 0, z: 0, width: 1, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: 0, height: 1, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: 1 },
+ { x: 0, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: 0 },
+ { x: 0, y: 0, z: 1, width: 0, height: 0, depthOrArrayLayers: -1 },
+ { x: 0, y: 0, z: 2, width: 0, height: 0, depthOrArrayLayers: -1 },
+ ])
+ .unless(
+ p =>
+ p.dimension === '1d' &&
+ (p.copyBoxOffsets.y !== 0 ||
+ p.copyBoxOffsets.z !== 0 ||
+ p.copyBoxOffsets.height !== 0 ||
+ p.copyBoxOffsets.depthOrArrayLayers !== 0)
+ )
+ .combine('srcCopyLevel', [0, 1, 3])
+ .combine('dstCopyLevel', [0, 1, 3])
+ .unless(p => p.dimension === '1d' && (p.srcCopyLevel !== 0 || p.dstCopyLevel !== 0))
+ )
+ .fn(async t => {
+ const { dimension, copyBoxOffsets, srcCopyLevel, dstCopyLevel } = t.params;
+
+ const textureSize = { width: 16, height: 8, depthOrArrayLayers: 3 };
+ let mipLevelCount = 4;
+ if (dimension === '1d') {
+ mipLevelCount = 1;
+ textureSize.height = 1;
+ textureSize.depthOrArrayLayers = 1;
+ }
+ const kFormat = 'rgba8unorm';
+
+ const srcTexture = t.device.createTexture({
+ size: textureSize,
+ format: kFormat,
+ dimension,
+ mipLevelCount,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ const dstTexture = t.device.createTexture({
+ size: textureSize,
+ format: kFormat,
+ dimension,
+ mipLevelCount,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ const srcSizeAtLevel = t.GetPhysicalSubresourceSize(
+ dimension,
+ textureSize,
+ kFormat,
+ srcCopyLevel
+ );
+ const dstSizeAtLevel = t.GetPhysicalSubresourceSize(
+ dimension,
+ textureSize,
+ kFormat,
+ dstCopyLevel
+ );
+
+ const copyOrigin = { x: copyBoxOffsets.x, y: copyBoxOffsets.y, z: copyBoxOffsets.z };
+
+ const copyWidth = Math.max(
+ Math.min(srcSizeAtLevel.width, dstSizeAtLevel.width) + copyBoxOffsets.width - copyOrigin.x,
+ 0
+ );
+ const copyHeight = Math.max(
+ Math.min(srcSizeAtLevel.height, dstSizeAtLevel.height) + copyBoxOffsets.height - copyOrigin.y,
+ 0
+ );
+ const copyDepth =
+ textureSize.depthOrArrayLayers + copyBoxOffsets.depthOrArrayLayers - copyOrigin.z;
+
+ {
+ let isSuccess =
+ copyWidth <= srcSizeAtLevel.width &&
+ copyHeight <= srcSizeAtLevel.height &&
+ copyOrigin.x + copyWidth <= dstSizeAtLevel.width &&
+ copyOrigin.y + copyHeight <= dstSizeAtLevel.height;
+
+ if (dimension === '3d') {
+ isSuccess =
+ isSuccess &&
+ copyDepth <= srcSizeAtLevel.depthOrArrayLayers &&
+ copyOrigin.z + copyDepth <= dstSizeAtLevel.depthOrArrayLayers;
+ } else {
+ isSuccess =
+ isSuccess &&
+ copyDepth <= textureSize.depthOrArrayLayers &&
+ copyOrigin.z + copyDepth <= textureSize.depthOrArrayLayers;
+ }
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: copyOrigin, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ }
+
+ {
+ let isSuccess =
+ copyOrigin.x + copyWidth <= srcSizeAtLevel.width &&
+ copyOrigin.y + copyHeight <= srcSizeAtLevel.height &&
+ copyWidth <= dstSizeAtLevel.width &&
+ copyHeight <= dstSizeAtLevel.height;
+
+ if (dimension === '3d') {
+ isSuccess =
+ isSuccess &&
+ copyDepth <= dstSizeAtLevel.depthOrArrayLayers &&
+ copyOrigin.z + copyDepth <= srcSizeAtLevel.depthOrArrayLayers;
+ } else {
+ isSuccess =
+ isSuccess &&
+ copyDepth <= textureSize.depthOrArrayLayers &&
+ copyOrigin.z + copyDepth <= textureSize.depthOrArrayLayers;
+ }
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: copyOrigin, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ }
+ });
+
+g.test('copy_within_same_texture')
+ .desc(
+ `
+Test that it is an error to use copyTextureToTexture from one subresource to itself.
+- for various starting source/destination array layers.
+- for various copy sizes in number of array layers
+
+TODO: Extend to check the copy is allowed between different mip levels.
+TODO: Extend to 1D and 3D textures.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('srcCopyOriginZ', [0, 2, 4])
+ .combine('dstCopyOriginZ', [0, 2, 4])
+ .combine('copyExtentDepth', [1, 2, 3])
+ )
+ .fn(async t => {
+ const { srcCopyOriginZ, dstCopyOriginZ, copyExtentDepth } = t.params;
+
+ const kArrayLayerCount = 7;
+
+ const testTexture = t.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: kArrayLayerCount },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const isSuccess =
+ Math.min(srcCopyOriginZ, dstCopyOriginZ) + copyExtentDepth <=
+ Math.max(srcCopyOriginZ, dstCopyOriginZ);
+ t.TestCopyTextureToTexture(
+ { texture: testTexture, origin: { x: 0, y: 0, z: srcCopyOriginZ } },
+ { texture: testTexture, origin: { x: 0, y: 0, z: dstCopyOriginZ } },
+ { width: 16, height: 16, depthOrArrayLayers: copyExtentDepth },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('copy_aspects')
+ .desc(
+ `
+Test the validations on the member 'aspect' of GPUImageCopyTexture in CopyTextureToTexture().
+- for all the color and depth-stencil formats: the texture copy aspects must be both 'all'.
+- for all the depth-only formats: the texture copy aspects must be either 'all' or 'depth-only'.
+- for all the stencil-only formats: the texture copy aspects must be either 'all' or 'stencil-only'.
+`
+ )
+ .params(u =>
+ u
+ .combine('format', ['rgba8unorm', ...kDepthStencilFormats] as const)
+ .beginSubcases()
+ .combine('sourceAspect', ['all', 'depth-only', 'stencil-only'] as const)
+ .combine('destinationAspect', ['all', 'depth-only', 'stencil-only'] as const)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { format, sourceAspect, destinationAspect } = t.params;
+
+ const kTextureSize = { width: 16, height: 8, depthOrArrayLayers: 1 };
+
+ const srcTexture = t.device.createTexture({
+ size: kTextureSize,
+ format,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ const dstTexture = t.device.createTexture({
+ size: kTextureSize,
+ format,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ // MAINTENANCE_TODO: get the valid aspects from capability_info.ts.
+ const kValidAspectsForFormat = {
+ rgba8unorm: ['all'],
+
+ // kUnsizedDepthStencilFormats
+ depth24plus: ['all', 'depth-only'],
+ 'depth24plus-stencil8': ['all'],
+ 'depth32float-stencil8': ['all'],
+
+ // kSizedDepthStencilFormats
+ depth32float: ['all', 'depth-only'],
+ stencil8: ['all', 'stencil-only'],
+ depth16unorm: ['all', 'depth-only'],
+ };
+
+ const isSourceAspectValid = kValidAspectsForFormat[format].includes(sourceAspect);
+ const isDestinationAspectValid = kValidAspectsForFormat[format].includes(destinationAspect);
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: { x: 0, y: 0, z: 0 }, aspect: sourceAspect },
+ { texture: dstTexture, origin: { x: 0, y: 0, z: 0 }, aspect: destinationAspect },
+ kTextureSize,
+ isSourceAspectValid && isDestinationAspectValid ? 'Success' : 'FinishError'
+ );
+ });
+
+g.test('copy_ranges_with_compressed_texture_formats')
+ .desc(
+ `
+Test that copyTextureToTexture copy boxes must be in range of the subresource and aligned to the block size
+- for various dimensions
+- for various offsets to a full copy for the copy origin/size
+- for various copy mip levels
+
+TODO: Express the offsets in "block size" so as to be able to test non-4x4 compressed formats
+`
+ )
+ .params(u =>
+ u
+ .combine('format', kCompressedTextureFormats)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('copyBoxOffsets', [
+ { x: 0, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 1, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 4, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: -1, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: -4, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 1, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 4, z: 0, width: 0, height: 0, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: 0, height: -1, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: 0, height: -4, depthOrArrayLayers: -2 },
+ { x: 0, y: 0, z: 0, width: 0, height: 0, depthOrArrayLayers: 0 },
+ { x: 0, y: 0, z: 1, width: 0, height: 0, depthOrArrayLayers: -1 },
+ ])
+ .combine('srcCopyLevel', [0, 1, 2])
+ .combine('dstCopyLevel', [0, 1, 2])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { format, dimension, copyBoxOffsets, srcCopyLevel, dstCopyLevel } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+
+ const kTextureSize = {
+ width: 15 * blockWidth,
+ height: 12 * blockHeight,
+ depthOrArrayLayers: 3,
+ };
+ const kMipLevelCount = 4;
+
+ const srcTexture = t.device.createTexture({
+ size: kTextureSize,
+ format,
+ dimension,
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ const dstTexture = t.device.createTexture({
+ size: kTextureSize,
+ format,
+ dimension,
+ mipLevelCount: kMipLevelCount,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ const srcSizeAtLevel = t.GetPhysicalSubresourceSize(
+ dimension,
+ kTextureSize,
+ format,
+ srcCopyLevel
+ );
+ const dstSizeAtLevel = t.GetPhysicalSubresourceSize(
+ dimension,
+ kTextureSize,
+ format,
+ dstCopyLevel
+ );
+
+ const copyOrigin = { x: copyBoxOffsets.x, y: copyBoxOffsets.y, z: copyBoxOffsets.z };
+
+ const copyWidth = Math.max(
+ Math.min(srcSizeAtLevel.width, dstSizeAtLevel.width) + copyBoxOffsets.width - copyOrigin.x,
+ 0
+ );
+ const copyHeight = Math.max(
+ Math.min(srcSizeAtLevel.height, dstSizeAtLevel.height) + copyBoxOffsets.height - copyOrigin.y,
+ 0
+ );
+ const copyDepth =
+ kTextureSize.depthOrArrayLayers + copyBoxOffsets.depthOrArrayLayers - copyOrigin.z;
+
+ const texelBlockWidth = kTextureFormatInfo[format].blockWidth;
+ const texelBlockHeight = kTextureFormatInfo[format].blockHeight;
+
+ const isSuccessForCompressedFormats =
+ copyOrigin.x % texelBlockWidth === 0 &&
+ copyOrigin.y % texelBlockHeight === 0 &&
+ copyWidth % texelBlockWidth === 0 &&
+ copyHeight % texelBlockHeight === 0;
+
+ {
+ const isSuccess =
+ isSuccessForCompressedFormats &&
+ copyWidth <= srcSizeAtLevel.width &&
+ copyHeight <= srcSizeAtLevel.height &&
+ copyOrigin.x + copyWidth <= dstSizeAtLevel.width &&
+ copyOrigin.y + copyHeight <= dstSizeAtLevel.height &&
+ copyOrigin.z + copyDepth <= kTextureSize.depthOrArrayLayers;
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: copyOrigin, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ }
+
+ {
+ const isSuccess =
+ isSuccessForCompressedFormats &&
+ copyOrigin.x + copyWidth <= srcSizeAtLevel.width &&
+ copyOrigin.y + copyHeight <= srcSizeAtLevel.height &&
+ copyWidth <= dstSizeAtLevel.width &&
+ copyHeight <= dstSizeAtLevel.height &&
+ copyOrigin.z + copyDepth <= kTextureSize.depthOrArrayLayers;
+
+ t.TestCopyTextureToTexture(
+ { texture: srcTexture, origin: copyOrigin, mipLevel: srcCopyLevel },
+ { texture: dstTexture, origin: { x: 0, y: 0, z: 0 }, mipLevel: dstCopyLevel },
+ { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth },
+ isSuccess ? 'Success' : 'FinishError'
+ );
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/debug.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/debug.spec.ts
new file mode 100644
index 0000000000..c8a3bdbbe4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/debug.spec.ts
@@ -0,0 +1,64 @@
+export const description = `
+API validation test for debug groups and markers
+
+Test Coverage:
+ - For each encoder type (GPUCommandEncoder, GPUComputeEncoder, GPURenderPassEncoder,
+ GPURenderBundleEncoder):
+ - Test that all pushDebugGroup must have a corresponding popDebugGroup
+ - Push and pop counts of 0, 1, and 2 will be used.
+ - An error must be generated for non matching counts.
+ - Test calling pushDebugGroup with empty and non-empty strings.
+ - Test inserting a debug marker with empty and non-empty strings.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { kEncoderTypes } from '../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('debug_group_balanced')
+ .params(u =>
+ u
+ .combine('encoderType', kEncoderTypes)
+ .beginSubcases()
+ .combine('pushCount', [0, 1, 2])
+ .combine('popCount', [0, 1, 2])
+ )
+ .fn(t => {
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(t.params.encoderType);
+ for (let i = 0; i < t.params.pushCount; ++i) {
+ encoder.pushDebugGroup(`${i}`);
+ }
+ for (let i = 0; i < t.params.popCount; ++i) {
+ encoder.popDebugGroup();
+ }
+ validateFinishAndSubmit(t.params.pushCount === t.params.popCount, true);
+ });
+
+g.test('debug_group')
+ .params(u =>
+ u //
+ .combine('encoderType', kEncoderTypes)
+ .beginSubcases()
+ .combine('label', ['', 'group'])
+ )
+ .fn(t => {
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(t.params.encoderType);
+ encoder.pushDebugGroup(t.params.label);
+ encoder.popDebugGroup();
+ validateFinishAndSubmit(true, true);
+ });
+
+g.test('debug_marker')
+ .params(u =>
+ u //
+ .combine('encoderType', kEncoderTypes)
+ .beginSubcases()
+ .combine('label', ['', 'marker'])
+ )
+ .fn(t => {
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(t.params.encoderType);
+ encoder.insertDebugMarker(t.params.label);
+ validateFinishAndSubmit(true, true);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/index_access.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/index_access.spec.ts
new file mode 100644
index 0000000000..cdd7159d15
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/index_access.spec.ts
@@ -0,0 +1,162 @@
+export const description = `
+Validation tests for indexed draws accessing the index buffer.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ createIndexBuffer(indexData: Iterable<number>): GPUBuffer {
+ return this.makeBufferWithContents(new Uint32Array(indexData), GPUBufferUsage.INDEX);
+ }
+
+ createRenderPipeline(): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: {
+ topology: 'triangle-strip',
+ stripIndexFormat: 'uint32',
+ },
+ });
+ }
+
+ beginRenderPass(encoder: GPUCommandEncoder) {
+ const colorAttachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ return encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+
+ drawIndexed(
+ indexBuffer: GPUBuffer,
+ indexCount: number,
+ instanceCount: number,
+ firstIndex: number,
+ baseVertex: number,
+ firstInstance: number,
+ isSuccess: boolean
+ ) {
+ const pipeline = this.createRenderPipeline();
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = this.beginRenderPass(encoder);
+ pass.setPipeline(pipeline);
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ pass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
+ pass.end();
+
+ if (isSuccess) {
+ this.device.queue.submit([encoder.finish()]);
+ } else {
+ this.expectValidationError(() => {
+ encoder.finish();
+ });
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('out_of_bounds')
+ .desc(
+ `Test drawing with out of bound index access to make sure encoder validation catch the
+ following indexCount and firstIndex OOB conditions
+ - either is within bound but indexCount + firstIndex is out of bound
+ - only firstIndex is out of bound
+ - only indexCount is out of bound
+ - firstIndex much larger than indexCount
+ - indexCount much larger than firstIndex
+ - max uint32 value for both to make sure the sum doesn't overflow
+ - max uint32 indexCount and small firstIndex
+ - max uint32 firstIndex and small indexCount
+ Together with normal and large instanceCount`
+ )
+ .params(
+ u =>
+ u
+ .combineWithParams([
+ { indexCount: 6, firstIndex: 0 }, // draw all 6 out of 6 index
+ { indexCount: 5, firstIndex: 1 }, // draw the last 5 out of 6 index
+ { indexCount: 1, firstIndex: 5 }, // draw the last 1 out of 6 index
+ { indexCount: 0, firstIndex: 6 }, // firstIndex point to the one after last, but (indexCount + firstIndex) * stride <= bufferSize, valid
+ { indexCount: 0, firstIndex: 7 }, // (indexCount + firstIndex) * stride > bufferSize, invalid
+ { indexCount: 7, firstIndex: 0 }, // only indexCount out of bound
+ { indexCount: 6, firstIndex: 1 }, // indexCount + firstIndex out of bound
+ { indexCount: 1, firstIndex: 6 }, // indexCount valid, but (indexCount + firstIndex) out of bound
+ { indexCount: 6, firstIndex: 10000 }, // firstIndex much larger than the bound
+ { indexCount: 10000, firstIndex: 0 }, // indexCount much larger than the bound
+ { indexCount: 0xffffffff, firstIndex: 0xffffffff }, // max uint32 value
+ { indexCount: 0xffffffff, firstIndex: 2 }, // max uint32 indexCount and small firstIndex
+ { indexCount: 2, firstIndex: 0xffffffff }, // small indexCount and max uint32 firstIndex
+ ] as const)
+ .combine('instanceCount', [1, 10000]) // normal and large instanceCount
+ )
+ .fn(t => {
+ const { indexCount, firstIndex, instanceCount } = t.params;
+
+ const indexBuffer = t.createIndexBuffer([0, 1, 2, 3, 1, 2]);
+ const isSuccess: boolean = indexCount + firstIndex <= 6;
+
+ t.drawIndexed(indexBuffer, indexCount, instanceCount, firstIndex, 0, 0, isSuccess);
+ });
+
+g.test('out_of_bounds_zero_sized_index_buffer')
+ .desc(
+ `Test drawing with an empty index buffer to make sure the encoder validation catch the
+ following indexCount and firstIndex conditions
+ - indexCount + firstIndex is out of bound
+ - indexCount is 0 but firstIndex is out of bound
+ - only indexCount is out of bound
+ - both are 0s (not out of bound) but index buffer size is 0
+ Together with normal and large instanceCount`
+ )
+ .params(
+ u =>
+ u
+ .combineWithParams([
+ { indexCount: 3, firstIndex: 1 }, // indexCount + firstIndex out of bound
+ { indexCount: 0, firstIndex: 1 }, // indexCount is 0 but firstIndex out of bound
+ { indexCount: 3, firstIndex: 0 }, // only indexCount out of bound
+ { indexCount: 0, firstIndex: 0 }, // just zeros, valid
+ ] as const)
+ .combine('instanceCount', [1, 10000]) // normal and large instanceCount
+ )
+ .fn(t => {
+ const { indexCount, firstIndex, instanceCount } = t.params;
+
+ const indexBuffer = t.createIndexBuffer([]);
+ const isSuccess: boolean = indexCount + firstIndex <= 0;
+
+ t.drawIndexed(indexBuffer, indexCount, instanceCount, firstIndex, 0, 0, isSuccess);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
new file mode 100644
index 0000000000..913ea86f33
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/draw.spec.ts
@@ -0,0 +1,862 @@
+export const description = `
+Here we test the validation for draw functions, mainly the buffer access validation. All four types
+of draw calls are tested, and test that validation errors do / don't occur for certain call type
+and parameters as expect.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kVertexFormatInfo } from '../../../../../capability_info.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+type VertexAttrib<A> = A & { shaderLocation: number };
+type VertexBuffer<V, A> = V & {
+ slot: number;
+ attributes: VertexAttrib<A>[];
+};
+type VertexState<V, A> = VertexBuffer<V, A>[];
+
+type VertexLayoutState<V, A> = VertexState<
+ { stepMode: GPUVertexStepMode; arrayStride: number } & V,
+ { format: GPUVertexFormat; offset: number } & A
+>;
+
+interface DrawIndexedParameter {
+ indexCount: number;
+ instanceCount?: number;
+ firstIndex?: number;
+ baseVertex?: number;
+ firstInstance?: number;
+}
+
+function callDrawIndexed(
+ test: GPUTest,
+ encoder: GPURenderCommandsMixin,
+ drawType: 'drawIndexed' | 'drawIndexedIndirect',
+ param: DrawIndexedParameter
+) {
+ switch (drawType) {
+ case 'drawIndexed': {
+ encoder.drawIndexed(
+ param.indexCount,
+ param.instanceCount ?? 1,
+ param.firstIndex ?? 0,
+ param.baseVertex ?? 0,
+ param.firstInstance ?? 0
+ );
+ break;
+ }
+ case 'drawIndexedIndirect': {
+ const indirectArray = new Int32Array([
+ param.indexCount,
+ param.instanceCount ?? 1,
+ param.firstIndex ?? 0,
+ param.baseVertex ?? 0,
+ param.firstInstance ?? 0,
+ ]);
+ const indirectBuffer = test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ break;
+ }
+ }
+}
+interface DrawParameter {
+ vertexCount: number;
+ instanceCount?: number;
+ firstVertex?: number;
+ firstInstance?: number;
+}
+
+function callDraw(
+ test: GPUTest,
+ encoder: GPURenderCommandsMixin,
+ drawType: 'draw' | 'drawIndirect',
+ param: DrawParameter
+) {
+ switch (drawType) {
+ case 'draw': {
+ encoder.draw(
+ param.vertexCount,
+ param.instanceCount ?? 1,
+ param.firstVertex ?? 0,
+ param.firstInstance ?? 0
+ );
+ break;
+ }
+ case 'drawIndirect': {
+ const indirectArray = new Int32Array([
+ param.vertexCount,
+ param.instanceCount ?? 1,
+ param.firstVertex ?? 0,
+ param.firstInstance ?? 0,
+ ]);
+ const indirectBuffer = test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ encoder.drawIndirect(indirectBuffer, 0);
+ break;
+ }
+ }
+}
+
+function makeTestPipeline(
+ test: ValidationTest,
+ buffers: VertexState<
+ { stepMode: GPUVertexStepMode; arrayStride: number },
+ {
+ offset: number;
+ format: GPUVertexFormat;
+ }
+ >
+): GPURenderPipeline {
+ const bufferLayouts: GPUVertexBufferLayout[] = [];
+ for (const b of buffers) {
+ bufferLayouts[b.slot] = b;
+ }
+
+ return test.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: test.device.createShaderModule({
+ code: test.getNoOpShaderCode('VERTEX'),
+ }),
+ entryPoint: 'main',
+ buffers: bufferLayouts,
+ },
+ fragment: {
+ module: test.device.createShaderModule({
+ code: test.getNoOpShaderCode('FRAGMENT'),
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+}
+
+function makeTestPipelineWithVertexAndInstanceBuffer(
+ test: ValidationTest,
+ arrayStride: number,
+ attributeFormat: GPUVertexFormat,
+ attributeOffset: number = 0
+): GPURenderPipeline {
+ const vertexBufferLayouts: VertexLayoutState<{}, {}> = [
+ {
+ slot: 1,
+ stepMode: 'vertex',
+ arrayStride,
+ attributes: [
+ {
+ shaderLocation: 2,
+ format: attributeFormat,
+ offset: attributeOffset,
+ },
+ ],
+ },
+ {
+ slot: 7,
+ stepMode: 'instance',
+ arrayStride,
+ attributes: [
+ {
+ shaderLocation: 6,
+ format: attributeFormat,
+ offset: attributeOffset,
+ },
+ ],
+ },
+ ];
+
+ return makeTestPipeline(test, vertexBufferLayouts);
+}
+
+// Default parameters for all kind of draw call, arbitrary non-zero values that is not very large.
+const kDefaultParameterForDraw = {
+ instanceCount: 100,
+ firstInstance: 100,
+};
+
+// Default parameters for non-indexed draw, arbitrary non-zero values that is not very large.
+const kDefaultParameterForNonIndexedDraw = {
+ vertexCount: 100,
+ firstVertex: 100,
+};
+
+// Default parameters for indexed draw call and required index buffer, arbitrary non-zero values
+// that is not very large.
+const kDefaultParameterForIndexedDraw = {
+ indexCount: 100,
+ firstIndex: 100,
+ baseVertex: 100,
+ indexFormat: 'uint16' as GPUIndexFormat,
+ indexBufferSize: 2 * 200, // exact required bound size for index buffer
+};
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test(`unused_buffer_bound`)
+ .desc(
+ `
+In this test we test that a small buffer bound to unused buffer slot won't cause validation error.
+- All draw commands,
+ - An unused {index , vertex} buffer with uselessly small range is bound (immediately before draw
+ call)
+`
+ )
+ .params(u =>
+ u //
+ .combine('smallIndexBuffer', [false, true])
+ .combine('smallVertexBuffer', [false, true])
+ .combine('smallInstanceBuffer', [false, true])
+ .beginSubcases()
+ .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ .unless(
+ // Always provide index buffer of enough size if it is used by indexed draw
+ p =>
+ p.smallIndexBuffer &&
+ (p.drawType === 'drawIndexed' || p.drawType === 'drawIndexedIndirect')
+ )
+ .combine('bufferOffset', [0, 4])
+ .combine('boundSize', [0, 1])
+ )
+ .fn(async t => {
+ const {
+ smallIndexBuffer,
+ smallVertexBuffer,
+ smallInstanceBuffer,
+ drawType,
+ bufferOffset,
+ boundSize,
+ } = t.params;
+ const renderPipeline = t.createNoOpRenderPipeline();
+ const bufferSize = bufferOffset + boundSize;
+ const smallBuffer = t.createBufferWithState('valid', {
+ size: bufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX,
+ });
+
+ // An index buffer of enough size, used if smallIndexBuffer === false
+ const { indexFormat, indexBufferSize } = kDefaultParameterForIndexedDraw;
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: indexBufferSize,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'drawIndexed' || drawType === 'drawIndexedIndirect') {
+ // Always use large enough index buffer for indexed draw. Index buffer OOB validation is
+ // tested in index_buffer_OOB.
+ renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, indexBufferSize);
+ } else if (smallIndexBuffer) {
+ renderEncoder.setIndexBuffer(smallBuffer, indexFormat, bufferOffset, boundSize);
+ }
+ if (smallVertexBuffer) {
+ renderEncoder.setVertexBuffer(1, smallBuffer, bufferOffset, boundSize);
+ }
+ if (smallInstanceBuffer) {
+ renderEncoder.setVertexBuffer(7, smallBuffer, bufferOffset, boundSize);
+ }
+
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForNonIndexedDraw,
+ };
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const drawParam: DrawIndexedParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForIndexedDraw,
+ };
+ callDrawIndexed(t, renderEncoder, drawType, drawParam);
+ }
+
+ // Binding a unused small index/vertex buffer will never cause validation error.
+ commandBufferMaker.validateFinishAndSubmit(true, true);
+ }
+ }
+ });
+
+g.test(`index_buffer_OOB`)
+ .desc(
+ `
+In this test we test that index buffer OOB is caught as a validation error in drawIndexed, but not in
+drawIndexedIndirect as it is GPU-validated.
+- Issue an indexed draw call, with the following index buffer states, for {all index formats}:
+ - range and GPUBuffer are exactly the required size for the draw call
+ - range is too small but GPUBuffer is still large enough
+ - range and GPUBuffer are both too small
+`
+ )
+ .params(u =>
+ u
+ .combine('bufferSizeInElements', [10, 100])
+ // Binding size is always no larger than buffer size, make sure that setIndexBuffer succeed
+ .combine('bindingSizeInElements', [10])
+ .combine('drawIndexCount', [10, 11])
+ .combine('drawType', ['drawIndexed', 'drawIndexedIndirect'] as const)
+ .beginSubcases()
+ .combine('indexFormat', ['uint16', 'uint32'] as GPUIndexFormat[])
+ )
+ .fn(async t => {
+ const {
+ indexFormat,
+ bindingSizeInElements,
+ bufferSizeInElements,
+ drawIndexCount,
+ drawType,
+ } = t.params;
+
+ const indexElementSize = indexFormat === 'uint16' ? 2 : 4;
+ const bindingSize = bindingSizeInElements * indexElementSize;
+ const bufferSize = bufferSizeInElements * indexElementSize;
+
+ const desc: GPUBufferDescriptor = {
+ size: bufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+ };
+ const indexBuffer = t.createBufferWithState('valid', desc);
+
+ const drawCallParam: DrawIndexedParameter = {
+ indexCount: drawIndexCount,
+ };
+
+ // Encoder finish will succeed if no index buffer access OOB when calling drawIndexed,
+ // and always succeed when calling drawIndexedIndirect.
+ const isFinishSuccess =
+ drawIndexCount <= bindingSizeInElements || drawType === 'drawIndexedIndirect';
+
+ const renderPipeline = t.createNoOpRenderPipeline();
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, bindingSize);
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ callDrawIndexed(t, renderEncoder, drawType, drawCallParam);
+
+ commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
+ }
+ }
+ });
+
+g.test(`vertex_buffer_OOB`)
+ .desc(
+ `
+In this test we test the vertex buffer OOB validation in draw calls. Specifically, only vertex step
+mode buffer OOB in draw and instance step mode buffer OOB in draw and drawIndexed are CPU-validated.
+Other cases are handled by robust access and no validation error occurs.
+- Test that:
+ - Draw call needs to read {=, >} any bound vertex buffer range, with GPUBuffer that is {large
+ enough, exactly the size of bound range}
+ - Binding size = 0 (ensure it's not treated as a special case)
+ - x= weird buffer offset values
+ - x= weird attribute offset values
+ - x= weird arrayStride values
+ - x= {render pass, render bundle}
+- For vertex step mode vertex buffer,
+ - Test that:
+ - vertexCount largeish
+ - firstVertex {=, >} 0
+ - arrayStride is 0 and bound buffer size too small
+ - (vertexCount + firstVertex) is zero
+ - Validation error occurs in:
+ - draw
+ - drawIndexed with a zero array stride vertex step mode buffer OOB
+ - Otherwise no validation error in drawIndexed, draIndirect and drawIndexedIndirect
+- For instance step mode vertex buffer,
+ - Test with draw and drawIndexed:
+ - instanceCount largeish
+ - firstInstance {=, >} 0
+ - arrayStride is 0 and bound buffer size too small
+ - (instanceCount + firstInstance) is zero
+ - Validation error occurs in draw and drawIndexed
+ - No validation error in drawIndirect and drawIndexedIndirect
+
+In this test, we use a a render pipeline requiring one vertex step mode with different vertex buffer
+layouts (attribute offset, array stride, vertex format). Then for a given drawing parameter set (e.g.,
+vertexCount, instanceCount, firstVertex, indexCount), we calculate the exactly required size for
+vertex step mode vertex buffer. Then, we generate buffer parameters (i.e. GPU buffer size,
+binding offset and binding size) for all buffers, covering both (bound size == required size),
+(bound size == required size - 1), and (bound size == 0), and test that draw and drawIndexed will
+success/error as expected. Such set of buffer parameters should include cases like weird offset values.
+`
+ )
+ .params(u =>
+ u
+ // type of draw call
+ .combine('type', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ // the state of vertex step mode vertex buffer bound size
+ .combine('VBSize', ['zero', 'exile', 'enough'] as const)
+ // the state of instance step mode vertex buffer bound size
+ .combine('IBSize', ['zero', 'exile', 'enough'] as const)
+ // should the vertex stride count be zero
+ .combine('VStride0', [false, true] as const)
+ // should the instance stride count be zero
+ .combine('IStride0', [false, true] as const)
+ // the state of array stride
+ .combine('AStride', ['zero', 'exact', 'oversize'] as const)
+ // the factor for offset of attributes in vertex layout
+ .combine('offset', [0, 1, 2, 7]) // the offset of attribute will be factor * MIN(4, sizeof(vertexFormat))
+ .beginSubcases()
+ .combine('setBufferOffset', [0, 200]) // must be a multiple of 4
+ .combine('attributeFormat', ['snorm8x2', 'float32', 'float16x4'] as GPUVertexFormat[])
+ .combine('vertexCount', [0, 1, 10000])
+ .combine('firstVertex', [0, 10000])
+ .filter(p => p.VStride0 === (p.firstVertex + p.vertexCount === 0))
+ .combine('instanceCount', [0, 1, 10000])
+ .combine('firstInstance', [0, 10000])
+ .filter(p => p.IStride0 === (p.firstInstance + p.instanceCount === 0))
+ .unless(p => p.vertexCount === 10000 && p.instanceCount === 10000)
+ )
+ .fn(async t => {
+ const {
+ type: drawType,
+ VBSize: boundVertexBufferSizeState,
+ IBSize: boundInstanceBufferSizeState,
+ VStride0: zeroVertexStrideCount,
+ IStride0: zeroInstanceStrideCount,
+ AStride: arrayStrideState,
+ offset: attributeOffsetFactor,
+ setBufferOffset,
+ attributeFormat,
+ vertexCount,
+ instanceCount,
+ firstVertex,
+ firstInstance,
+ } = t.params;
+
+ const attributeFormatInfo = kVertexFormatInfo[attributeFormat];
+ const formatSize = attributeFormatInfo.bytesPerComponent * attributeFormatInfo.componentCount;
+ const attributeOffset = attributeOffsetFactor * Math.min(4, formatSize);
+ const lastStride = attributeOffset + formatSize;
+ let arrayStride = 0;
+ if (arrayStrideState !== 'zero') {
+ arrayStride = lastStride;
+ if (arrayStrideState === 'oversize') {
+ // Add an arbitrary number to array stride to make it larger than required by attributes
+ arrayStride = arrayStride + 20;
+ }
+ arrayStride = arrayStride + (-arrayStride & 3); // Make sure arrayStride is a multiple of 4
+ }
+
+ const calcSetBufferSize = (
+ boundBufferSizeState: 'zero' | 'exile' | 'enough',
+ strideCount: number
+ ): number => {
+ let requiredBufferSize: number;
+ if (strideCount > 0) {
+ requiredBufferSize = arrayStride * (strideCount - 1) + lastStride;
+ } else {
+ // Spec do not validate bounded buffer size if strideCount == 0.
+ requiredBufferSize = lastStride;
+ }
+ let setBufferSize: number;
+ switch (boundBufferSizeState) {
+ case 'zero': {
+ setBufferSize = 0;
+ break;
+ }
+ case 'exile': {
+ setBufferSize = requiredBufferSize - 1;
+ break;
+ }
+ case 'enough': {
+ setBufferSize = requiredBufferSize;
+ break;
+ }
+ }
+ return setBufferSize;
+ };
+
+ const strideCountForVertexBuffer = firstVertex + vertexCount;
+ const setVertexBufferSize = calcSetBufferSize(
+ boundVertexBufferSizeState,
+ strideCountForVertexBuffer
+ );
+ const vertexBufferSize = setBufferOffset + setVertexBufferSize;
+ const strideCountForInstanceBuffer = firstInstance + instanceCount;
+ const setInstanceBufferSize = calcSetBufferSize(
+ boundInstanceBufferSizeState,
+ strideCountForInstanceBuffer
+ );
+ const instanceBufferSize = setBufferOffset + setInstanceBufferSize;
+
+ const vertexBuffer = t.createBufferWithState('valid', {
+ size: vertexBufferSize,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ const instanceBuffer = t.createBufferWithState('valid', {
+ size: instanceBufferSize,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const renderPipeline = makeTestPipelineWithVertexAndInstanceBuffer(
+ t,
+ arrayStride,
+ attributeFormat,
+ attributeOffset
+ );
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setVertexBuffer(1, vertexBuffer, setBufferOffset, setVertexBufferSize);
+ renderEncoder.setVertexBuffer(7, instanceBuffer, setBufferOffset, setInstanceBufferSize);
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ vertexCount,
+ instanceCount,
+ firstVertex,
+ firstInstance,
+ };
+
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const {
+ indexFormat,
+ indexCount,
+ firstIndex,
+ indexBufferSize,
+ } = kDefaultParameterForIndexedDraw;
+
+ const desc: GPUBufferDescriptor = {
+ size: indexBufferSize,
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
+ };
+ const indexBuffer = t.createBufferWithState('valid', desc);
+
+ const drawParam: DrawIndexedParameter = {
+ indexCount,
+ instanceCount,
+ firstIndex,
+ baseVertex: firstVertex,
+ firstInstance,
+ };
+
+ renderEncoder.setIndexBuffer(indexBuffer, indexFormat, 0, indexBufferSize);
+ callDrawIndexed(t, renderEncoder, drawType, drawParam);
+ }
+
+ const isVertexBufferOOB =
+ boundVertexBufferSizeState !== 'enough' &&
+ drawType === 'draw' && // drawIndirect, drawIndexed, and drawIndexedIndirect do not validate vertex step mode buffer
+ !zeroVertexStrideCount; // vertex step mode buffer never OOB if stride count = 0
+ const isInstanceBufferOOB =
+ boundInstanceBufferSizeState !== 'enough' &&
+ (drawType === 'draw' || drawType === 'drawIndexed') && // drawIndirect and drawIndexedIndirect do not validate instance step mode buffer
+ !zeroInstanceStrideCount; // vertex step mode buffer never OOB if stride count = 0
+ const isFinishSuccess = !isVertexBufferOOB && !isInstanceBufferOOB;
+
+ commandBufferMaker.validateFinishAndSubmit(isFinishSuccess, true);
+ }
+ }
+ });
+
+g.test(`buffer_binding_overlap`)
+ .desc(
+ `
+In this test we test that binding one GPU buffer to multiple vertex buffer slot or both vertex
+buffer slot and index buffer will cause no validation error, with completely/partial overlap.
+ - x= all draw types
+`
+ )
+ .params(u =>
+ u //
+ .combine('drawType', ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const)
+ .beginSubcases()
+ .combine('vertexBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('instanceBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('indexBoundOffestFactor', [0, 0.5, 1, 1.5, 2])
+ .combine('arrayStrideState', ['zero', 'exact', 'oversize'] as const)
+ )
+ .fn(async t => {
+ const {
+ drawType,
+ vertexBoundOffestFactor,
+ instanceBoundOffestFactor,
+ indexBoundOffestFactor,
+ arrayStrideState,
+ } = t.params;
+
+ // Compute the array stride for vertex step mode and instance step mode attribute
+ const attributeFormat = 'float32x4';
+ const attributeFormatInfo = kVertexFormatInfo[attributeFormat];
+ const formatSize = attributeFormatInfo.bytesPerComponent * attributeFormatInfo.componentCount;
+ const attributeOffset = 0;
+ const lastStride = attributeOffset + formatSize;
+ let arrayStride = 0;
+ if (arrayStrideState !== 'zero') {
+ arrayStride = lastStride;
+ if (arrayStrideState === 'oversize') {
+ // Add an arbitrary number to array stride
+ arrayStride = arrayStride + 20;
+ }
+ arrayStride = arrayStride + (-arrayStride & 3); // Make sure arrayStride is a multiple of 4
+ }
+
+ const calcAttributeBufferSize = (strideCount: number): number => {
+ let requiredBufferSize: number;
+ if (strideCount > 0) {
+ requiredBufferSize = arrayStride * (strideCount - 1) + lastStride;
+ } else {
+ // Spec do not validate bounded buffer size if strideCount == 0.
+ requiredBufferSize = lastStride;
+ }
+ return requiredBufferSize;
+ };
+
+ const calcSetBufferOffset = (requiredSetBufferSize: number, offsetFactor: number): number => {
+ const offset = Math.ceil(requiredSetBufferSize * offsetFactor);
+ const alignedOffset = offset + (-offset & 3); // Make sure offset is a multiple of 4
+ return alignedOffset;
+ };
+
+ // Compute required bound range for all vertex and index buffer to ensure the shared GPU buffer
+ // has enough size.
+ const { vertexCount, firstVertex } = kDefaultParameterForNonIndexedDraw;
+ const strideCountForVertexBuffer = firstVertex + vertexCount;
+ const setVertexBufferSize = calcAttributeBufferSize(strideCountForVertexBuffer);
+ const setVertexBufferOffset = calcSetBufferOffset(setVertexBufferSize, vertexBoundOffestFactor);
+ let requiredBufferSize = setVertexBufferOffset + setVertexBufferSize;
+
+ const { instanceCount, firstInstance } = kDefaultParameterForDraw;
+ const strideCountForInstanceBuffer = firstInstance + instanceCount;
+ const setInstanceBufferSize = calcAttributeBufferSize(strideCountForInstanceBuffer);
+ const setInstanceBufferOffset = calcSetBufferOffset(
+ setInstanceBufferSize,
+ instanceBoundOffestFactor
+ );
+ requiredBufferSize = Math.max(
+ requiredBufferSize,
+ setInstanceBufferOffset + setInstanceBufferSize
+ );
+
+ const { indexBufferSize: setIndexBufferSize, indexFormat } = kDefaultParameterForIndexedDraw;
+ const setIndexBufferOffset = calcSetBufferOffset(setIndexBufferSize, indexBoundOffestFactor);
+ requiredBufferSize = Math.max(requiredBufferSize, setIndexBufferOffset + setIndexBufferSize);
+
+ // Create the shared GPU buffer with both vertetx and index usage
+ const sharedBuffer = t.createBufferWithState('valid', {
+ size: requiredBufferSize,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.INDEX,
+ });
+
+ const renderPipeline = makeTestPipelineWithVertexAndInstanceBuffer(
+ t,
+ arrayStride,
+ attributeFormat
+ );
+
+ for (const encoderType of ['render bundle', 'render pass'] as const) {
+ for (const setPipelineBeforeBuffer of [false, true]) {
+ const commandBufferMaker = t.createEncoder(encoderType);
+ const renderEncoder = commandBufferMaker.encoder;
+
+ if (setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+ renderEncoder.setVertexBuffer(1, sharedBuffer, setVertexBufferOffset, setVertexBufferSize);
+ renderEncoder.setVertexBuffer(
+ 7,
+ sharedBuffer,
+ setInstanceBufferOffset,
+ setInstanceBufferSize
+ );
+ renderEncoder.setIndexBuffer(
+ sharedBuffer,
+ indexFormat,
+ setIndexBufferOffset,
+ setIndexBufferSize
+ );
+ if (!setPipelineBeforeBuffer) {
+ renderEncoder.setPipeline(renderPipeline);
+ }
+
+ if (drawType === 'draw' || drawType === 'drawIndirect') {
+ const drawParam: DrawParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForNonIndexedDraw,
+ };
+ callDraw(t, renderEncoder, drawType, drawParam);
+ } else {
+ const drawParam: DrawIndexedParameter = {
+ ...kDefaultParameterForDraw,
+ ...kDefaultParameterForIndexedDraw,
+ };
+ callDrawIndexed(t, renderEncoder, drawType, drawParam);
+ }
+
+ // Since all bound buffer are of enough size, draw call should always succeed.
+ commandBufferMaker.validateFinishAndSubmit(true, true);
+ }
+ }
+ });
+
+g.test(`last_buffer_setting_take_account`)
+ .desc(
+ `
+In this test we test that only the last setting for a buffer slot take account.
+- All (non/indexed, in/direct) draw commands
+ - setPl, setVB, setIB, draw, {setPl,setVB,setIB,nothing (control)}, then a larger draw that
+ wouldn't have been valid before that
+`
+ )
+ .unimplemented();
+
+g.test(`max_draw_count`)
+ .desc(
+ `
+In this test we test that draw count which exceeds
+GPURenderPassDescriptor.maxDrawCount causes validation error on
+GPUCommandEncoder.finish(). The test sets specified maxDrawCount,
+calls specified draw call specified times with or without bundles,
+and checks whether GPUCommandEncoder.finish() causes a validation error.
+ - x= whether to use a bundle for the first half of the draw calls
+ - x= whether to use a bundle for the second half of the draw calls
+ - x= several different draw counts
+ - x= several different maxDrawCounts
+`
+ )
+ .params(u =>
+ u
+ .combine('bundleFirstHalf', [false, true])
+ .combine('bundleSecondHalf', [false, true])
+ .combine('maxDrawCount', [0, 1, 4, 16])
+ .beginSubcases()
+ .expand('drawCount', p => new Set([0, p.maxDrawCount, p.maxDrawCount + 1]))
+ )
+ .fn(async t => {
+ const { bundleFirstHalf, bundleSecondHalf, maxDrawCount, drawCount } = t.params;
+
+ const colorFormat = 'rgba8unorm';
+ const colorTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: colorFormat,
+ mipLevelCount: 1,
+ sampleCount: 1,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() {}`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorFormat, writeMask: 0 }],
+ },
+ });
+
+ const indexBuffer = t.makeBufferWithContents(new Uint16Array([0, 0, 0]), GPUBufferUsage.INDEX);
+ const indirectBuffer = t.makeBufferWithContents(
+ new Uint32Array([3, 1, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ );
+ const indexedIndirectBuffer = t.makeBufferWithContents(
+ new Uint32Array([3, 1, 0, 0, 0]),
+ GPUBufferUsage.INDIRECT
+ );
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPassEncoder = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ maxDrawCount,
+ });
+
+ const firstHalfEncoder = bundleFirstHalf
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: [colorFormat],
+ })
+ : renderPassEncoder;
+
+ const secondHalfEncoder = bundleSecondHalf
+ ? t.device.createRenderBundleEncoder({
+ colorFormats: [colorFormat],
+ })
+ : renderPassEncoder;
+
+ firstHalfEncoder.setPipeline(pipeline);
+ firstHalfEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ secondHalfEncoder.setPipeline(pipeline);
+ secondHalfEncoder.setIndexBuffer(indexBuffer, 'uint16');
+
+ const halfDrawCount = Math.floor(drawCount / 2);
+ for (let i = 0; i < drawCount; i++) {
+ const encoder = i < halfDrawCount ? firstHalfEncoder : secondHalfEncoder;
+ if (i % 4 === 0) {
+ encoder.draw(3);
+ }
+ if (i % 4 === 1) {
+ encoder.drawIndexed(3);
+ }
+ if (i % 4 === 2) {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ if (i % 4 === 3) {
+ encoder.drawIndexedIndirect(indexedIndirectBuffer, 0);
+ }
+ }
+
+ const bundles = [];
+ if (bundleFirstHalf) {
+ bundles.push((firstHalfEncoder as GPURenderBundleEncoder).finish());
+ }
+ if (bundleSecondHalf) {
+ bundles.push((secondHalfEncoder as GPURenderBundleEncoder).finish());
+ }
+
+ if (bundles.length > 0) {
+ renderPassEncoder.executeBundles(bundles);
+ }
+
+ renderPassEncoder.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ }, drawCount > maxDrawCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts
new file mode 100644
index 0000000000..d7bdec6ba5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/dynamic_state.spec.ts
@@ -0,0 +1,319 @@
+export const description = `
+API validation tests for dynamic state commands (setViewport/ScissorRect/BlendColor...).
+
+TODO: ensure existing tests cover these notes. Note many of these may be operation tests instead.
+> - setViewport
+> - {x, y} = {0, invalid values if any}
+> - {width, height, minDepth, maxDepth} = {
+> - least possible value that's valid
+> - greatest possible negative value that's invalid
+> - greatest possible positive value that's valid
+> - least possible positive value that's invalid if any
+> - }
+> - minDepth {<, =, >} maxDepth
+> - setScissorRect
+> - {width, height} = 0
+> - {x+width, y+height} = attachment size + 1
+> - setBlendConstant
+> - color {slightly, very} out of range
+> - used with a simple pipeline that {does, doesn't} use it
+> - setStencilReference
+> - {0, max}
+> - used with a simple pipeline that {does, doesn't} use it
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+interface ViewportCall {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ minDepth: number;
+ maxDepth: number;
+}
+
+interface ScissorCall {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+}
+
+class F extends ValidationTest {
+ testViewportCall(
+ success: boolean,
+ v: ViewportCall,
+ attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
+ ) {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: attachmentSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setViewport(v.x, v.y, v.w, v.h, v.minDepth, v.maxDepth);
+ pass.end();
+
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+
+ testScissorCall(
+ success: boolean | 'type-error',
+ s: ScissorCall,
+ attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
+ ) {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: attachmentSize,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ if (success === 'type-error') {
+ this.shouldThrow('TypeError', () => {
+ pass.setScissorRect(s.x, s.y, s.w, s.h);
+ });
+ } else {
+ pass.setScissorRect(s.x, s.y, s.w, s.h);
+ pass.end();
+
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+ }
+
+ createDummyRenderPassEncoder(): { encoder: GPUCommandEncoder; pass: GPURenderPassEncoder } {
+ const attachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachment.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ return { encoder, pass };
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('setViewport,x_y_width_height_nonnegative')
+ .desc(
+ `Test that the parameters of setViewport to define the box must be non-negative.
+
+TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
+TODO Test the first value smaller than -0`
+ )
+ .paramsSubcasesOnly([
+ // Control case: everything to 0 is ok, covers the empty viewport case.
+ { x: 0, y: 0, w: 0, h: 0 },
+
+ // Test -1
+ { x: -1, y: 0, w: 0, h: 0 },
+ { x: 0, y: -1, w: 0, h: 0 },
+ { x: 0, y: 0, w: -1, h: 0 },
+ { x: 0, y: 0, w: 0, h: -1 },
+ ])
+ .fn(t => {
+ const { x, y, w, h } = t.params;
+ const success = x >= 0 && y >= 0 && w >= 0 && h >= 0;
+ t.testViewportCall(success, { x, y, w, h, minDepth: 0, maxDepth: 1 });
+ });
+
+g.test('setViewport,xy_rect_contained_in_attachment')
+ .desc(
+ 'Test that the rectangle defined by x, y, width, height must be contained in the attachments'
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combineWithParams([
+ { attachmentWidth: 3, attachmentHeight: 5 },
+ { attachmentWidth: 5, attachmentHeight: 3 },
+ { attachmentWidth: 1024, attachmentHeight: 1 },
+ { attachmentWidth: 1, attachmentHeight: 1024 },
+ ])
+ .combineWithParams([
+ // Control case: a full viewport is valid.
+ { dx: 0, dy: 0, dw: 0, dh: 0 },
+
+ // Other valid cases with a partial viewport.
+ { dx: 1, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: -1 },
+ { dx: 0, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: -1 },
+
+ // Test with a small value that causes the viewport to go outside the attachment.
+ { dx: 1, dy: 0, dw: 0, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: 0 },
+ { dx: 0, dy: 0, dw: 1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: 1 },
+ ])
+ )
+ .fn(t => {
+ const { attachmentWidth, attachmentHeight, dx, dy, dw, dh } = t.params;
+ const x = dx;
+ const y = dy;
+ const w = attachmentWidth + dw;
+ const h = attachmentWidth + dh;
+
+ const success = x + w <= attachmentWidth && y + h <= attachmentHeight;
+ t.testViewportCall(
+ success,
+ { x, y, w, h, minDepth: 0, maxDepth: 1 },
+ { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
+ );
+ });
+
+g.test('setViewport,depth_rangeAndOrder')
+ .desc('Test that 0 <= minDepth <= maxDepth <= 1')
+ .paramsSubcasesOnly([
+ // Success cases
+ { minDepth: 0, maxDepth: 1 },
+ { minDepth: -0, maxDepth: -0 },
+ { minDepth: 1, maxDepth: 1 },
+ { minDepth: 0.3, maxDepth: 0.7 },
+ { minDepth: 0.7, maxDepth: 0.7 },
+ { minDepth: 0.3, maxDepth: 0.3 },
+
+ // Invalid cases
+ { minDepth: -0.1, maxDepth: 1 },
+ { minDepth: 0, maxDepth: 1.1 },
+ { minDepth: 0.5, maxDepth: 0.49999 },
+ ])
+ .fn(t => {
+ const { minDepth, maxDepth } = t.params;
+ const success =
+ 0 <= minDepth && minDepth <= 1 && 0 <= maxDepth && maxDepth <= 1 && minDepth <= maxDepth;
+ t.testViewportCall(success, { x: 0, y: 0, w: 1, h: 1, minDepth, maxDepth });
+ });
+
+g.test('setScissorRect,x_y_width_height_nonnegative')
+ .desc(
+ `Test that the parameters of setScissorRect to define the box must be non-negative or a TypeError is thrown.
+
+TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
+TODO Test the first value smaller than -0`
+ )
+ .paramsSubcasesOnly([
+ // Control case: everything to 0 is ok, covers the empty scissor case.
+ { x: 0, y: 0, w: 0, h: 0 },
+
+ // Test -1
+ { x: -1, y: 0, w: 0, h: 0 },
+ { x: 0, y: -1, w: 0, h: 0 },
+ { x: 0, y: 0, w: -1, h: 0 },
+ { x: 0, y: 0, w: 0, h: -1 },
+ ])
+ .fn(t => {
+ const { x, y, w, h } = t.params;
+ const success = x >= 0 && y >= 0 && w >= 0 && h >= 0;
+ t.testScissorCall(success ? true : 'type-error', { x, y, w, h });
+ });
+
+g.test('setScissorRect,xy_rect_contained_in_attachment')
+ .desc(
+ 'Test that the rectangle defined by x, y, width, height must be contained in the attachments'
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combineWithParams([
+ { attachmentWidth: 3, attachmentHeight: 5 },
+ { attachmentWidth: 5, attachmentHeight: 3 },
+ { attachmentWidth: 1024, attachmentHeight: 1 },
+ { attachmentWidth: 1, attachmentHeight: 1024 },
+ ])
+ .combineWithParams([
+ // Control case: a full scissor is valid.
+ { dx: 0, dy: 0, dw: 0, dh: 0 },
+
+ // Other valid cases with a partial scissor.
+ { dx: 1, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: -1 },
+ { dx: 0, dy: 0, dw: -1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: -1 },
+
+ // Test with a small value that causes the scissor to go outside the attachment.
+ { dx: 1, dy: 0, dw: 0, dh: 0 },
+ { dx: 0, dy: 1, dw: 0, dh: 0 },
+ { dx: 0, dy: 0, dw: 1, dh: 0 },
+ { dx: 0, dy: 0, dw: 0, dh: 1 },
+ ])
+ )
+ .fn(t => {
+ const { attachmentWidth, attachmentHeight, dx, dy, dw, dh } = t.params;
+ const x = dx;
+ const y = dy;
+ const w = attachmentWidth + dw;
+ const h = attachmentWidth + dh;
+
+ const success = x + w <= attachmentWidth && y + h <= attachmentHeight;
+ t.testScissorCall(
+ success,
+ { x, y, w, h },
+ { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
+ );
+ });
+
+g.test('setBlendConstant')
+ .desc('Test that almost any color value is valid for setBlendConstant')
+ .paramsSubcasesOnly([
+ { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
+ { r: -1.0, g: -1.0, b: -1.0, a: -1.0 },
+ { r: Number.MAX_SAFE_INTEGER, g: Number.MIN_SAFE_INTEGER, b: -0, a: 100000 },
+ ])
+ .fn(t => {
+ const { r, g, b, a } = t.params;
+ const encoders = t.createDummyRenderPassEncoder();
+ encoders.pass.setBlendConstant({ r, g, b, a });
+ encoders.pass.end();
+ encoders.encoder.finish();
+ });
+
+g.test('setStencilReference')
+ .desc('Test that almost any stencil reference value is valid for setStencilReference')
+ .paramsSubcasesOnly([
+ { value: 1 }, //
+ { value: 0 },
+ { value: 1000 },
+ { value: 0xffffffff },
+ ])
+ .fn(t => {
+ const { value } = t.params;
+ const encoders = t.createDummyRenderPassEncoder();
+ encoders.pass.setStencilReference(value);
+ encoders.pass.end();
+ encoders.encoder.finish();
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts
new file mode 100644
index 0000000000..017c1aa24f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/indirect_draw.spec.ts
@@ -0,0 +1,202 @@
+export const description = `
+Validation tests for drawIndirect/drawIndexedIndirect on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams } from './render.js';
+
+const kIndirectDrawTestParams = kRenderEncodeTypeParams.combine('indexed', [true, false] as const);
+
+class F extends ValidationTest {
+ makeIndexBuffer(): GPUBuffer {
+ return this.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('indirect_buffer_state')
+ .desc(
+ `
+Tests indirect buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, indexed, state } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.createBufferWithState(state, {
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('indirect_buffer,device_mismatch')
+ .desc(
+ 'Tests draw(Indexed)Indirect cannot be called with an indirect buffer created from another device'
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, indexed, mismatched } = t.params;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const indirectBuffer = sourceDevice.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+ t.trackForCleanup(indirectBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(t.createNoOpRenderPipeline());
+
+ if (indexed) {
+ encoder.setIndexBuffer(t.makeIndexBuffer(), 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ validateFinish(!mismatched);
+ });
+
+g.test('indirect_buffer_usage')
+ .desc(
+ `
+Tests indirect buffer must have 'Indirect' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kIndirectDrawTestParams.combine('usage', [
+ GPUConst.BufferUsage.INDIRECT, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.INDIRECT,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, indexed, usage } = t.params;
+ const indirectBuffer = t.device.createBuffer({
+ size: 256,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(t.createNoOpRenderPipeline());
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, 0);
+ } else {
+ encoder.drawIndirect(indirectBuffer, 0);
+ }
+ validateFinish((usage & GPUBufferUsage.INDIRECT) !== 0);
+ });
+
+g.test('indirect_offset_alignment')
+ .desc(
+ `
+Tests indirect offset must be a multiple of 4.
+ `
+ )
+ .paramsSubcasesOnly(kIndirectDrawTestParams.combine('indirectOffset', [0, 2, 4] as const))
+ .fn(t => {
+ const { encoderType, indexed, indirectOffset } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, indirectOffset);
+ } else {
+ encoder.drawIndirect(indirectBuffer, indirectOffset);
+ }
+
+ validateFinish(indirectOffset % 4 === 0);
+ });
+
+g.test('indirect_offset_oob')
+ .desc(
+ `
+Tests indirect draw calls with various indirect offsets and buffer sizes.
+- (offset, b.size) is
+ - (0, 0)
+ - (0, min size) (control case)
+ - (0, min size + 1) (control case)
+ - (0, min size - 1)
+ - (0, min size - min alignment)
+ - (min alignment, min size + min alignment)
+ - (min alignment, min size + min alignment - 1)
+ - (min alignment / 2, min size + min alignment)
+ - (min alignment +/- 1, min size + min alignment)
+ - (min size, min size)
+ - (min size + min alignment, min size)
+ - min size = indirect draw parameters size
+ - x =(drawIndirect, drawIndexedIndirect)
+ `
+ )
+ .paramsSubcasesOnly(
+ kIndirectDrawTestParams.expandWithParams(p => {
+ const indirectParamsSize = p.indexed ? 20 : 16;
+ return [
+ { indirectOffset: 0, bufferSize: 0, _valid: false },
+ { indirectOffset: 0, bufferSize: indirectParamsSize, _valid: true },
+ { indirectOffset: 0, bufferSize: indirectParamsSize + 1, _valid: true },
+ { indirectOffset: 0, bufferSize: indirectParamsSize - 1, _valid: false },
+ { indirectOffset: 0, bufferSize: indirectParamsSize - 4, _valid: false },
+ { indirectOffset: 4, bufferSize: indirectParamsSize + 4, _valid: true },
+ { indirectOffset: 4, bufferSize: indirectParamsSize + 3, _valid: false },
+ { indirectOffset: 2, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: 3, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: 5, bufferSize: indirectParamsSize + 4, _valid: false },
+ { indirectOffset: indirectParamsSize, bufferSize: indirectParamsSize, _valid: false },
+ { indirectOffset: indirectParamsSize + 4, bufferSize: indirectParamsSize, _valid: false },
+ ] as const;
+ })
+ )
+ .fn(t => {
+ const { encoderType, indexed, indirectOffset, bufferSize, _valid } = t.params;
+ const pipeline = t.createNoOpRenderPipeline();
+ const indirectBuffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ if (indexed) {
+ const indexBuffer = t.makeIndexBuffer();
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ encoder.drawIndexedIndirect(indirectBuffer, indirectOffset);
+ } else {
+ encoder.drawIndirect(indirectBuffer, indirectOffset);
+ }
+
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts
new file mode 100644
index 0000000000..0df9ec6365
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/render.ts
@@ -0,0 +1,29 @@
+import { kUnitCaseParamsBuilder } from '../../../../../../common/framework/params_builder.js';
+import { kRenderEncodeTypes } from '../../../../../util/command_buffer_maker.js';
+
+export const kRenderEncodeTypeParams = kUnitCaseParamsBuilder.combine(
+ 'encoderType',
+ kRenderEncodeTypes
+);
+
+export function buildBufferOffsetAndSizeOOBTestParams(minAlignment: number, bufferSize: number) {
+ return kRenderEncodeTypeParams.combineWithParams([
+ // Explicit size
+ { offset: 0, size: 0, _valid: true },
+ { offset: 0, size: 1, _valid: true },
+ { offset: 0, size: 4, _valid: true },
+ { offset: 0, size: 5, _valid: true },
+ { offset: 0, size: bufferSize, _valid: true },
+ { offset: 0, size: bufferSize + 4, _valid: false },
+ { offset: minAlignment, size: bufferSize, _valid: false },
+ { offset: minAlignment, size: bufferSize - minAlignment, _valid: true },
+ { offset: bufferSize - minAlignment, size: minAlignment, _valid: true },
+ { offset: bufferSize, size: 1, _valid: false },
+ // Implicit size: buffer.size - offset
+ { offset: 0, size: undefined, _valid: true },
+ { offset: minAlignment, size: undefined, _valid: true },
+ { offset: bufferSize - minAlignment, size: undefined, _valid: true },
+ { offset: bufferSize, size: undefined, _valid: true },
+ { offset: bufferSize + minAlignment, size: undefined, _valid: false },
+ ]);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts
new file mode 100644
index 0000000000..1aacd8de90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setIndexBuffer.spec.ts
@@ -0,0 +1,124 @@
+export const description = `
+Validation tests for setIndexBuffer on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams, buildBufferOffsetAndSizeOOBTestParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('index_buffer_state')
+ .desc(
+ `
+Tests index buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const indexBuffer = t.createBufferWithState(state, {
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('index_buffer,device_mismatch')
+ .desc('Tests setIndexBuffer cannot be called with an index buffer created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const indexBuffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+ t.trackForCleanup(indexBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinish(!mismatched);
+ });
+
+g.test('index_buffer_usage')
+ .desc(
+ `
+Tests index buffer must have 'Index' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('usage', [
+ GPUConst.BufferUsage.INDEX, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.INDEX,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, usage } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32');
+ validateFinish((usage & GPUBufferUsage.INDEX) !== 0);
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+Tests offset must be a multiple of index format’s byte size.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams
+ .combine('indexFormat', ['uint16', 'uint32'] as const)
+ .expand('offset', p => {
+ return p.indexFormat === 'uint16' ? ([0, 1, 2] as const) : ([0, 2, 4] as const);
+ })
+ )
+ .fn(t => {
+ const { encoderType, indexFormat, offset } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, indexFormat, offset);
+
+ const alignment =
+ indexFormat === 'uint16' ? Uint16Array.BYTES_PER_ELEMENT : Uint32Array.BYTES_PER_ELEMENT;
+ validateFinish(offset % alignment === 0);
+ });
+
+g.test('offset_and_size_oob')
+ .desc(
+ `
+Tests offset and size cannot be larger than index buffer size.
+ `
+ )
+ .paramsSubcasesOnly(buildBufferOffsetAndSizeOOBTestParams(4, 256))
+ .fn(t => {
+ const { encoderType, offset, size, _valid } = t.params;
+ const indexBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.INDEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setIndexBuffer(indexBuffer, 'uint32', offset, size);
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts
new file mode 100644
index 0000000000..6fcd8015d3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setPipeline.spec.ts
@@ -0,0 +1,62 @@
+export const description = `
+Validation tests for setPipeline on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kRenderEncodeTypes } from '../../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('invalid_pipeline')
+ .desc(
+ `
+Tests setPipeline should generate an error iff using an 'invalid' pipeline.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('encoderType', kRenderEncodeTypes).combine('state', ['valid', 'invalid'] as const)
+ )
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const pipeline = t.createRenderPipelineWithState(state);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ validateFinish(state !== 'invalid');
+ });
+
+g.test('pipeline,device_mismatch')
+ .desc('Tests setPipeline cannot be called with a render pipeline created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const pipeline = sourceDevice.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: sourceDevice.createShaderModule({
+ code: `@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: sourceDevice.createShaderModule({
+ code: '@fragment fn main() {}',
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setPipeline(pipeline);
+ validateFinish(!mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts
new file mode 100644
index 0000000000..453281dbdd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/setVertexBuffer.spec.ts
@@ -0,0 +1,141 @@
+export const description = `
+Validation tests for setVertexBuffer on render pass and render bundle.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { kLimitInfo } from '../../../../../capability_info.js';
+import { GPUConst } from '../../../../../constants.js';
+import { kResourceStates } from '../../../../../gpu_test.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+import { kRenderEncodeTypeParams, buildBufferOffsetAndSizeOOBTestParams } from './render.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('slot')
+ .desc(
+ `
+Tests slot must be less than the maxVertexBuffers in device limits.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('slot', [
+ 0,
+ kLimitInfo.maxVertexBuffers.default - 1,
+ kLimitInfo.maxVertexBuffers.default,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, slot } = t.params;
+ const vertexBuffer = t.createBufferWithState('valid', {
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(slot, vertexBuffer);
+ validateFinish(slot < kLimitInfo.maxVertexBuffers.default);
+ });
+
+g.test('vertex_buffer_state')
+ .desc(
+ `
+Tests vertex buffer must be valid.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('state', kResourceStates))
+ .fn(t => {
+ const { encoderType, state } = t.params;
+ const vertexBuffer = t.createBufferWithState(state, {
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinishAndSubmitGivenState } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinishAndSubmitGivenState(state);
+ });
+
+g.test('vertex_buffer,device_mismatch')
+ .desc('Tests setVertexBuffer cannot be called with a vertex buffer created from another device')
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const vertexBuffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ t.trackForCleanup(vertexBuffer);
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinish(!mismatched);
+ });
+
+g.test('vertex_buffer_usage')
+ .desc(
+ `
+Tests vertex buffer must have 'Vertex' usage.
+ `
+ )
+ .paramsSubcasesOnly(
+ kRenderEncodeTypeParams.combine('usage', [
+ GPUConst.BufferUsage.VERTEX, // control case
+ GPUConst.BufferUsage.COPY_DST,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.VERTEX,
+ ] as const)
+ )
+ .fn(t => {
+ const { encoderType, usage } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer);
+ validateFinish((usage & GPUBufferUsage.VERTEX) !== 0);
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+Tests offset must be a multiple of 4.
+ `
+ )
+ .paramsSubcasesOnly(kRenderEncodeTypeParams.combine('offset', [0, 2, 4] as const))
+ .fn(t => {
+ const { encoderType, offset } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish: finish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer, offset);
+ finish(offset % 4 === 0);
+ });
+
+g.test('offset_and_size_oob')
+ .desc(
+ `
+Tests offset and size cannot be larger than vertex buffer size.
+ `
+ )
+ .paramsSubcasesOnly(buildBufferOffsetAndSizeOOBTestParams(4, 256))
+ .fn(t => {
+ const { encoderType, offset, size, _valid } = t.params;
+ const vertexBuffer = t.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.VERTEX,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setVertexBuffer(0, vertexBuffer, offset, size);
+ validateFinish(_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts
new file mode 100644
index 0000000000..310f96a9df
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render/state_tracking.spec.ts
@@ -0,0 +1,184 @@
+export const description = `
+Validation tests for setVertexBuffer/setIndexBuffer state (not validation). See also operation tests.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { range } from '../../../../../../common/util/util.js';
+import { ValidationTest } from '../../../validation_test.js';
+
+class F extends ValidationTest {
+ getVertexBuffer(): GPUBuffer {
+ return this.device.createBuffer({
+ size: 256,
+ usage: GPUBufferUsage.VERTEX,
+ });
+ }
+
+ createRenderPipeline(bufferCount: number): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ struct Inputs {
+ ${range(bufferCount, i => `\n@location(${i}) a_position${i} : vec3<f32>,`).join('')}
+ };
+ @vertex fn main(input : Inputs
+ ) -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ buffers: [
+ {
+ arrayStride: 3 * 4,
+ attributes: range(bufferCount, i => ({
+ format: 'float32x3',
+ offset: 0,
+ shaderLocation: i,
+ })),
+ },
+ ],
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ }
+
+ beginRenderPass(commandEncoder: GPUCommandEncoder): GPURenderPassEncoder {
+ const attachmentTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ return commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachmentTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test(`all_needed_vertex_buffer_should_be_bound`)
+ .desc(
+ `
+In this test we test that any missing vertex buffer for a used slot will cause validation errors when drawing.
+- All (non/indexed, in/direct) draw commands
+ - A needed vertex buffer is not bound
+ - Was bound in another render pass but not the current one
+`
+ )
+ .unimplemented();
+
+g.test(`all_needed_index_buffer_should_be_bound`)
+ .desc(
+ `
+In this test we test that missing index buffer for a used slot will cause validation errors when drawing.
+- All indexed in/direct draw commands
+ - No index buffer is bound
+`
+ )
+ .unimplemented();
+
+g.test('vertex_buffers_inherit_from_previous_pipeline').fn(async t => {
+ const pipeline1 = t.createRenderPipeline(1);
+ const pipeline2 = t.createRenderPipeline(2);
+
+ const vertexBuffer1 = t.getVertexBuffer();
+ const vertexBuffer2 = t.getVertexBuffer();
+
+ {
+ // Check failure when vertex buffer is not set
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ }
+ {
+ // Check success when vertex buffer is inherited from previous pipeline
+ const commandEncoder = t.device.createCommandEncoder();
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+
+ commandEncoder.finish();
+ }
+});
+
+g.test('vertex_buffers_do_not_inherit_between_render_passes').fn(async t => {
+ const pipeline1 = t.createRenderPipeline(1);
+ const pipeline2 = t.createRenderPipeline(2);
+
+ const vertexBuffer1 = t.getVertexBuffer();
+ const vertexBuffer2 = t.getVertexBuffer();
+
+ {
+ // Check success when vertex buffer is set for each render pass
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ commandEncoder.finish();
+ }
+ {
+ // Check failure because vertex buffer is not inherited in second subpass
+ const commandEncoder = t.device.createCommandEncoder();
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline2);
+ renderPass.setVertexBuffer(0, vertexBuffer1);
+ renderPass.setVertexBuffer(1, vertexBuffer2);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+ {
+ const renderPass = t.beginRenderPass(commandEncoder);
+ renderPass.setPipeline(pipeline1);
+ renderPass.draw(3);
+ renderPass.end();
+ }
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ });
+ }
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render_pass.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render_pass.spec.ts
new file mode 100644
index 0000000000..e3e881e01d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/render_pass.spec.ts
@@ -0,0 +1,14 @@
+export const description = `
+Validation tests for render pass encoding.
+Does **not** test usage scopes (resource_usages/), GPUProgrammablePassEncoder (programmable_pass),
+dynamic state (dynamic_render_state.spec.ts), or GPURenderEncoderBase (render.spec.ts).
+
+TODO:
+- executeBundles:
+ - with {zero, one, multiple} bundles where {zero, one} of them are invalid objects
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts
new file mode 100644
index 0000000000..476ad576e1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/cmds/setBindGroup.spec.ts
@@ -0,0 +1,446 @@
+export const description = `
+setBindGroup validation tests.
+
+TODO: merge these notes and implement.
+> (Note: If there are errors with using certain binding types in certain passes, test those in the file for that pass type, not here.)
+>
+> - state tracking (probably separate file)
+> - x= {compute pass, render pass}
+> - {null, compatible, incompatible} current pipeline (should have no effect without draw/dispatch)
+> - setBindGroup in different orders (e.g. 0,1,2 vs 2,0,1)
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { range, unreachable } from '../../../../../common/util/util.js';
+import {
+ kBufferBindingTypes,
+ kMinDynamicBufferOffsetAlignment,
+ kLimitInfo,
+} from '../../../../capability_info.js';
+import { kResourceStates, ResourceState } from '../../../../gpu_test.js';
+import {
+ kProgrammableEncoderTypes,
+ ProgrammableEncoderType,
+} from '../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ encoderTypeToStageFlag(encoderType: ProgrammableEncoderType): GPUShaderStageFlags {
+ switch (encoderType) {
+ case 'compute pass':
+ return GPUShaderStage.COMPUTE;
+ case 'render pass':
+ case 'render bundle':
+ return GPUShaderStage.FRAGMENT;
+ default:
+ unreachable('Unknown encoder type');
+ }
+ }
+
+ createBindingResourceWithState(
+ resourceType: 'texture' | 'buffer',
+ state: 'valid' | 'destroyed'
+ ): GPUBindingResource {
+ switch (resourceType) {
+ case 'texture': {
+ const texture = this.createTextureWithState('valid');
+ const view = texture.createView();
+ if (state === 'destroyed') {
+ texture.destroy();
+ }
+ return view;
+ }
+ case 'buffer':
+ return {
+ buffer: this.createBufferWithState(state, {
+ size: 4,
+ usage: GPUBufferUsage.STORAGE,
+ }),
+ };
+ default:
+ unreachable('unknown resource type');
+ }
+ }
+
+ /**
+ * If state is 'invalid', creates an invalid bind group with valid resources.
+ * If state is 'destroyed', creates a valid bind group with destroyed resources.
+ */
+ createBindGroup(
+ state: ResourceState,
+ resourceType: 'buffer' | 'texture',
+ encoderType: ProgrammableEncoderType,
+ indices: number[]
+ ) {
+ if (state === 'invalid') {
+ this.device.pushErrorScope('validation');
+ indices = new Array<number>(indices.length + 1).fill(0);
+ }
+
+ const layout = this.device.createBindGroupLayout({
+ entries: indices.map(binding => ({
+ binding,
+ visibility: this.encoderTypeToStageFlag(encoderType),
+ ...(resourceType === 'buffer' ? { buffer: { type: 'storage' } } : { texture: {} }),
+ })),
+ });
+ const bindGroup = this.device.createBindGroup({
+ layout,
+ entries: indices.map(binding => ({
+ binding,
+ resource: this.createBindingResourceWithState(
+ resourceType,
+ state === 'destroyed' ? state : 'valid'
+ ),
+ })),
+ });
+
+ if (state === 'invalid') {
+ void this.device.popErrorScope();
+ }
+ return bindGroup;
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('state_and_binding_index')
+ .desc('Tests that setBindGroup correctly handles {valid, invalid, destroyed} bindGroups.')
+ .params(u =>
+ u
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .combine('state', kResourceStates)
+ .combine('resourceType', ['buffer', 'texture'] as const)
+ )
+ .fn(async t => {
+ const { encoderType, state, resourceType } = t.params;
+ const maxBindGroups = t.device.limits.maxBindGroups;
+
+ async function runTest(index: number) {
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType);
+ encoder.setBindGroup(index, t.createBindGroup(state, resourceType, encoderType, [index]));
+
+ validateFinishAndSubmit(state !== 'invalid' && index < maxBindGroups, state !== 'destroyed');
+ }
+
+ // MAINTENANCE_TODO: move to subcases() once we can query the device limits
+ for (const index of [1, maxBindGroups - 1, maxBindGroups]) {
+ t.debug(`test bind group index ${index}`);
+ await runTest(index);
+ }
+ });
+
+g.test('bind_group,device_mismatch')
+ .desc(
+ `
+ Tests setBindGroup cannot be called with a bind group created from another device
+ - x= setBindGroup {sequence overload, Uint32Array overload}
+ `
+ )
+ .params(u =>
+ u
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .beginSubcases()
+ .combine('useU32Array', [true, false])
+ .combine('mismatched', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { encoderType, useU32Array, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const buffer = sourceDevice.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ const layout = sourceDevice.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: t.encoderTypeToStageFlag(encoderType),
+ buffer: { type: 'storage', hasDynamicOffset: useU32Array },
+ },
+ ],
+ });
+
+ const bindGroup = sourceDevice.createBindGroup({
+ layout,
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer },
+ },
+ ],
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ if (useU32Array) {
+ encoder.setBindGroup(0, bindGroup, new Uint32Array([0]), 0, 1);
+ } else {
+ encoder.setBindGroup(0, bindGroup);
+ }
+ validateFinish(!mismatched);
+ });
+
+g.test('dynamic_offsets_passed_but_not_expected')
+ .desc('Tests that setBindGroup correctly errors on unexpected dynamicOffsets.')
+ .params(u => u.combine('encoderType', kProgrammableEncoderTypes))
+ .fn(async t => {
+ const { encoderType } = t.params;
+ const bindGroup = t.createBindGroup('valid', 'buffer', encoderType, []);
+ const dynamicOffsets = [0];
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setBindGroup(0, bindGroup, dynamicOffsets);
+ validateFinish(false);
+ });
+
+g.test('dynamic_offsets_match_expectations_in_pass_encoder')
+ .desc('Tests that given dynamicOffsets match the specified bindGroup.')
+ .params(u =>
+ u
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .combineWithParams([
+ { dynamicOffsets: [256, 0], _success: true }, // Dynamic offsets aligned
+ { dynamicOffsets: [1, 2], _success: false }, // Dynamic offsets not aligned
+
+ // Wrong number of dynamic offsets
+ { dynamicOffsets: [256, 0, 0], _success: false },
+ { dynamicOffsets: [256], _success: false },
+ { dynamicOffsets: [], _success: false },
+
+ // Dynamic uniform buffer out of bounds because of binding size
+ { dynamicOffsets: [512, 0], _success: false },
+ { dynamicOffsets: [1024, 0], _success: false },
+ { dynamicOffsets: [0xffffffff, 0], _success: false },
+
+ // Dynamic storage buffer out of bounds because of binding size
+ { dynamicOffsets: [0, 512], _success: false },
+ { dynamicOffsets: [0, 1024], _success: false },
+ { dynamicOffsets: [0, 0xffffffff], _success: false },
+ ])
+ .combine('useU32array', [false, true])
+ )
+ .fn(async t => {
+ const kBindingSize = 12;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'uniform',
+ hasDynamicOffset: true,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'storage',
+ hasDynamicOffset: true,
+ },
+ },
+ ],
+ });
+
+ const uniformBuffer = t.device.createBuffer({
+ size: 2 * kMinDynamicBufferOffsetAlignment + 8,
+ usage: GPUBufferUsage.UNIFORM,
+ });
+
+ const storageBuffer = t.device.createBuffer({
+ size: 2 * kMinDynamicBufferOffsetAlignment + 8,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ size: kBindingSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: storageBuffer,
+ size: kBindingSize,
+ },
+ },
+ ],
+ });
+
+ const { encoderType, dynamicOffsets, useU32array, _success } = t.params;
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ if (useU32array) {
+ encoder.setBindGroup(0, bindGroup, new Uint32Array(dynamicOffsets), 0, dynamicOffsets.length);
+ } else {
+ encoder.setBindGroup(0, bindGroup, dynamicOffsets);
+ }
+ validateFinish(_success);
+ });
+
+g.test('u32array_start_and_length')
+ .desc('Tests that dynamicOffsetsData(Start|Length) apply to the given Uint32Array.')
+ .paramsSubcasesOnly([
+ // dynamicOffsetsDataLength > offsets.length
+ {
+ offsets: [0] as const,
+ dynamicOffsetsDataStart: 0,
+ dynamicOffsetsDataLength: 2,
+ _success: false,
+ },
+ // dynamicOffsetsDataStart + dynamicOffsetsDataLength > offsets.length
+ {
+ offsets: [0] as const,
+ dynamicOffsetsDataStart: 1,
+ dynamicOffsetsDataLength: 1,
+ _success: false,
+ },
+ {
+ offsets: [0, 0] as const,
+ dynamicOffsetsDataStart: 1,
+ dynamicOffsetsDataLength: 1,
+ _success: true,
+ },
+ {
+ offsets: [0, 0, 0] as const,
+ dynamicOffsetsDataStart: 1,
+ dynamicOffsetsDataLength: 1,
+ _success: true,
+ },
+ {
+ offsets: [0, 0] as const,
+ dynamicOffsetsDataStart: 0,
+ dynamicOffsetsDataLength: 2,
+ _success: true,
+ },
+ ])
+ .fn(t => {
+ const { offsets, dynamicOffsetsDataStart, dynamicOffsetsDataLength, _success } = t.params;
+ const kBindingSize = 8;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: range(dynamicOffsetsDataLength, i => ({
+ binding: i,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'storage',
+ hasDynamicOffset: true,
+ },
+ })),
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: range(dynamicOffsetsDataLength, i => ({
+ binding: i,
+ resource: {
+ buffer: t.createBufferWithState('valid', {
+ size: kBindingSize,
+ usage: GPUBufferUsage.STORAGE,
+ }),
+ size: kBindingSize,
+ },
+ })),
+ });
+
+ const { encoder, validateFinish } = t.createEncoder('render pass');
+
+ const doSetBindGroup = () => {
+ encoder.setBindGroup(
+ 0,
+ bindGroup,
+ new Uint32Array(offsets),
+ dynamicOffsetsDataStart,
+ dynamicOffsetsDataLength
+ );
+ };
+
+ if (_success) {
+ doSetBindGroup();
+ } else {
+ t.shouldThrow('RangeError', doSetBindGroup);
+ }
+
+ // RangeError in setBindGroup does not cause the encoder to become invalid.
+ validateFinish(true);
+ });
+
+g.test('buffer_dynamic_offsets')
+ .desc(
+ `
+ Test that the dynamic offsets of the BufferLayout is a multiple of
+ 'minUniformBufferOffsetAlignment|minStorageBufferOffsetAlignment' if the BindGroup entry defines
+ buffer and the buffer type is 'uniform|storage|read-only-storage'.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('type', kBufferBindingTypes)
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .beginSubcases()
+ .expand('dynamicOffset', ({ type }) =>
+ type === 'uniform'
+ ? [
+ kLimitInfo.minUniformBufferOffsetAlignment.default,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minUniformBufferOffsetAlignment.default * 2,
+ kLimitInfo.minUniformBufferOffsetAlignment.default + 2,
+ ]
+ : [
+ kLimitInfo.minStorageBufferOffsetAlignment.default,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 0.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 1.5,
+ kLimitInfo.minStorageBufferOffsetAlignment.default * 2,
+ kLimitInfo.minStorageBufferOffsetAlignment.default + 2,
+ ]
+ )
+ )
+ .fn(async t => {
+ const { type, dynamicOffset, encoderType } = t.params;
+ const kBindingSize = 12;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type, hasDynamicOffset: true },
+ },
+ ],
+ });
+
+ let usage, isValid;
+ if (type === 'uniform') {
+ usage = GPUBufferUsage.UNIFORM;
+ isValid = dynamicOffset % kLimitInfo.minUniformBufferOffsetAlignment.default === 0;
+ } else {
+ usage = GPUBufferUsage.STORAGE;
+ isValid = dynamicOffset % kLimitInfo.minStorageBufferOffsetAlignment.default === 0;
+ }
+
+ const buffer = t.device.createBuffer({
+ size: 3 * kMinDynamicBufferOffsetAlignment,
+ usage,
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ entries: [{ binding: 0, resource: { buffer, size: kBindingSize } }],
+ layout: bindGroupLayout,
+ });
+
+ const { encoder, validateFinish } = t.createEncoder(encoderType);
+ encoder.setBindGroup(0, bindGroup, [dynamicOffset]);
+ validateFinish(isValid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts
new file mode 100644
index 0000000000..11e411b1d0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/createRenderBundleEncoder.spec.ts
@@ -0,0 +1,240 @@
+export const description = `
+createRenderBundleEncoder validation tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import {
+ kAllTextureFormats,
+ kDepthStencilFormats,
+ kTextureFormatInfo,
+ kMaxColorAttachments,
+ kRenderableColorTextureFormats,
+} from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('attachment_state,limits,maxColorAttachments')
+ .desc(`Tests that attachment state must have <= device.limits.maxColorAttachments.`)
+ .params(u =>
+ u.beginSubcases().combine(
+ 'colorFormatCount',
+ range(kMaxColorAttachments + 1, i => i + 1) // 1-9
+ )
+ )
+ .fn(async t => {
+ const { colorFormatCount } = t.params;
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: Array(colorFormatCount).fill('r8unorm'),
+ });
+ }, colorFormatCount > t.device.limits.maxColorAttachments);
+ });
+
+g.test('attachment_state,limits,maxColorAttachmentBytesPerSample,aligned')
+ .desc(
+ `
+ Tests that the total color attachment bytes per sample <=
+ device.limits.maxColorAttachmentBytesPerSample when using the same format (aligned) for multiple
+ attachments.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine(
+ 'colorFormatCount',
+ range(kMaxColorAttachments, i => i + 1)
+ )
+ )
+ .fn(async t => {
+ const { format, colorFormatCount } = t.params;
+ const info = kTextureFormatInfo[format];
+ const shouldError =
+ info.renderTargetPixelByteCost === undefined ||
+ info.renderTargetPixelByteCost * colorFormatCount >
+ t.device.limits.maxColorAttachmentBytesPerSample;
+
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: Array(colorFormatCount).fill(format),
+ });
+ }, shouldError);
+ });
+
+g.test('attachment_state,limits,maxColorAttachmentBytesPerSample,unaligned')
+ .desc(
+ `
+ Tests that the total color attachment bytes per sample <=
+ device.limits.maxColorAttachmentBytesPerSample when using various sets of (potentially)
+ unaligned formats.
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ // Alignment causes the first 1 byte R8Unorm to become 4 bytes. So even though
+ // 1+4+8+16+1 < 32, the 4 byte alignment requirement of R32Float makes the first R8Unorm
+ // become 4 and 4+4+8+16+1 > 32. Re-ordering this so the R8Unorm's are at the end, however
+ // is allowed: 4+8+16+1+1 < 32.
+ {
+ formats: [
+ 'r8unorm',
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _shouldError: false,
+ },
+ {
+ formats: [
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _shouldError: true,
+ },
+ ])
+ )
+ .fn(async t => {
+ const { formats, _shouldError } = t.params;
+
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: formats,
+ });
+ }, _shouldError);
+ });
+
+g.test('attachment_state,empty_color_formats')
+ .desc(`Tests that if no colorFormats are given, a depthStencilFormat must be specified.`)
+ .params(u =>
+ u.beginSubcases().combine('depthStencilFormat', [undefined, 'depth24plus-stencil8'] as const)
+ )
+ .fn(async t => {
+ const { depthStencilFormat } = t.params;
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: [],
+ depthStencilFormat,
+ });
+ }, depthStencilFormat === undefined);
+ });
+
+g.test('valid_texture_formats')
+ .desc(
+ `
+ Tests that createRenderBundleEncoder only accepts valid formats for its attachments.
+ - colorFormats
+ - depthStencilFormat
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kAllTextureFormats)
+ .beginSubcases()
+ .combine('attachment', ['color', 'depthStencil'])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const { format, attachment } = t.params;
+
+ const colorRenderable =
+ kTextureFormatInfo[format].renderable && kTextureFormatInfo[format].color;
+
+ const depthStencil = kTextureFormatInfo[format].depth || kTextureFormatInfo[format].stencil;
+
+ switch (attachment) {
+ case 'color': {
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: [format],
+ });
+ }, !colorRenderable);
+
+ break;
+ }
+ case 'depthStencil': {
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: [],
+ depthStencilFormat: format,
+ });
+ }, !depthStencil);
+
+ break;
+ }
+ }
+ });
+
+g.test('depth_stencil_readonly')
+ .desc(
+ `
+ Tests that createRenderBundleEncoder validation of depthReadOnly and stencilReadOnly
+ - With depth-only formats
+ - With stencil-only formats
+ - With depth-stencil-combined formats
+ `
+ )
+ .params(u =>
+ u //
+ .combine('depthStencilFormat', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('depthReadOnly', [false, true])
+ .combine('stencilReadOnly', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const { depthStencilFormat } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(depthStencilFormat);
+ })
+ .fn(async t => {
+ const { depthStencilFormat, depthReadOnly, stencilReadOnly } = t.params;
+
+ let shouldError = false;
+ if (
+ kTextureFormatInfo[depthStencilFormat].depth &&
+ kTextureFormatInfo[depthStencilFormat].stencil &&
+ depthReadOnly !== stencilReadOnly
+ ) {
+ shouldError = true;
+ }
+
+ t.expectValidationError(() => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: [],
+ depthStencilFormat,
+ depthReadOnly,
+ stencilReadOnly,
+ });
+ }, shouldError);
+ });
+
+g.test('depth_stencil_readonly_with_undefined_depth')
+ .desc(
+ `
+ Tests that createRenderBundleEncoder validation of depthReadOnly and stencilReadOnly is ignored
+ if there is no depthStencilFormat set.
+ `
+ )
+ .params(u =>
+ u //
+ .beginSubcases()
+ .combine('depthReadOnly', [false, true])
+ .combine('stencilReadOnly', [false, true])
+ )
+ .fn(async t => {
+ const { depthReadOnly, stencilReadOnly } = t.params;
+
+ t.device.createRenderBundleEncoder({
+ colorFormats: ['bgra8unorm'],
+ depthReadOnly,
+ stencilReadOnly,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts
new file mode 100644
index 0000000000..0d56222eed
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_open_state.spec.ts
@@ -0,0 +1,587 @@
+export const description = `
+Validation tests to all commands of GPUCommandEncoder, GPUComputePassEncoder, and
+GPURenderPassEncoder when the encoder is not finished.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { keysOf } from '../../../../common/util/data_tables.js';
+import { unreachable } from '../../../../common/util/util.js';
+import { ValidationTest } from '../validation_test.js';
+
+import { beginRenderPassWithQuerySet } from './queries/common.js';
+
+class F extends ValidationTest {
+ createRenderPipelineForTest(): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `@fragment fn main() {}`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ });
+ }
+
+ createBindGroupForTest(): GPUBindGroup {
+ return this.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: this.device.createSampler(),
+ },
+ ],
+ layout: this.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ sampler: { type: 'filtering' },
+ },
+ ],
+ }),
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+type EncoderCommands = keyof Omit<GPUCommandEncoder, '__brand' | 'label' | 'finish'>;
+const kEncoderCommandInfo: {
+ readonly [k in EncoderCommands]: {};
+} = {
+ beginComputePass: {},
+ beginRenderPass: {},
+ clearBuffer: {},
+ copyBufferToBuffer: {},
+ copyBufferToTexture: {},
+ copyTextureToBuffer: {},
+ copyTextureToTexture: {},
+ insertDebugMarker: {},
+ popDebugGroup: {},
+ pushDebugGroup: {},
+ writeTimestamp: {},
+ resolveQuerySet: {},
+};
+const kEncoderCommands = keysOf(kEncoderCommandInfo);
+
+type RenderPassEncoderCommands = keyof Omit<GPURenderPassEncoder, '__brand' | 'label' | 'end'>;
+const kRenderPassEncoderCommandInfo: {
+ readonly [k in RenderPassEncoderCommands]: {};
+} = {
+ draw: {},
+ drawIndexed: {},
+ drawIndexedIndirect: {},
+ drawIndirect: {},
+ setIndexBuffer: {},
+ setBindGroup: {},
+ setVertexBuffer: {},
+ setPipeline: {},
+ setViewport: {},
+ setScissorRect: {},
+ setBlendConstant: {},
+ setStencilReference: {},
+ beginOcclusionQuery: {},
+ endOcclusionQuery: {},
+ executeBundles: {},
+ pushDebugGroup: {},
+ popDebugGroup: {},
+ insertDebugMarker: {},
+};
+const kRenderPassEncoderCommands = keysOf(kRenderPassEncoderCommandInfo);
+
+type RenderBundleEncoderCommands = keyof Omit<
+ GPURenderBundleEncoder,
+ '__brand' | 'label' | 'finish'
+>;
+const kRenderBundleEncoderCommandInfo: {
+ readonly [k in RenderBundleEncoderCommands]: {};
+} = {
+ draw: {},
+ drawIndexed: {},
+ drawIndexedIndirect: {},
+ drawIndirect: {},
+ setPipeline: {},
+ setBindGroup: {},
+ setIndexBuffer: {},
+ setVertexBuffer: {},
+ pushDebugGroup: {},
+ popDebugGroup: {},
+ insertDebugMarker: {},
+};
+const kRenderBundleEncoderCommands = keysOf(kRenderBundleEncoderCommandInfo);
+
+// MAINTENANCE_TODO: remove the deprecated 'dispatch' and 'dispatchIndirect' here once they're
+// removed from `@webgpu/types`.
+type ComputePassEncoderCommands = keyof Omit<
+ GPUComputePassEncoder,
+ '__brand' | 'label' | 'end' | 'dispatch' | 'dispatchIndirect'
+>;
+const kComputePassEncoderCommandInfo: {
+ readonly [k in ComputePassEncoderCommands]: {};
+} = {
+ setBindGroup: {},
+ setPipeline: {},
+ dispatchWorkgroups: {},
+ dispatchWorkgroupsIndirect: {},
+ pushDebugGroup: {},
+ popDebugGroup: {},
+ insertDebugMarker: {},
+};
+const kComputePassEncoderCommands = keysOf(kComputePassEncoderCommandInfo);
+
+g.test('non_pass_commands')
+ .desc(
+ `
+ Test that functions of GPUCommandEncoder generate a validation error if the encoder is already
+ finished.
+ `
+ )
+ .params(u =>
+ u
+ .combine('command', kEncoderCommands)
+ .beginSubcases()
+ .combine('finishBeforeCommand', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ switch (t.params.command) {
+ case 'writeTimestamp':
+ t.selectDeviceOrSkipTestCase('timestamp-query');
+ break;
+ }
+ })
+ .fn(t => {
+ const { command, finishBeforeCommand } = t.params;
+
+ const srcBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+ const dstBuffer = t.device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.QUERY_RESOLVE,
+ });
+
+ const textureSize = { width: 1, height: 1 };
+ const textureFormat = 'rgba8unorm';
+ const srcTexture = t.device.createTexture({
+ size: textureSize,
+ format: textureFormat,
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+ const dstTexture = t.device.createTexture({
+ size: textureSize,
+ format: textureFormat,
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ const querySet = t.device.createQuerySet({
+ type: command === 'writeTimestamp' ? 'timestamp' : 'occlusion',
+ count: 1,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+
+ if (finishBeforeCommand) encoder.finish();
+
+ t.expectValidationError(() => {
+ switch (command) {
+ case 'beginComputePass':
+ {
+ encoder.beginComputePass();
+ }
+ break;
+ case 'beginRenderPass':
+ {
+ encoder.beginRenderPass({ colorAttachments: [] });
+ }
+ break;
+ case 'clearBuffer':
+ {
+ encoder.clearBuffer(dstBuffer, 0, 16);
+ }
+ break;
+ case 'copyBufferToBuffer':
+ {
+ encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, 0);
+ }
+ break;
+ case 'copyBufferToTexture':
+ {
+ encoder.copyBufferToTexture(
+ { buffer: srcBuffer },
+ { texture: dstTexture },
+ textureSize
+ );
+ }
+ break;
+ case 'copyTextureToBuffer':
+ {
+ encoder.copyTextureToBuffer(
+ { texture: srcTexture },
+ { buffer: dstBuffer },
+ textureSize
+ );
+ }
+ break;
+ case 'copyTextureToTexture':
+ {
+ encoder.copyTextureToTexture(
+ { texture: srcTexture },
+ { texture: dstTexture },
+ textureSize
+ );
+ }
+ break;
+ case 'insertDebugMarker':
+ {
+ encoder.insertDebugMarker('marker');
+ }
+ break;
+ case 'pushDebugGroup':
+ {
+ encoder.pushDebugGroup('group');
+ }
+ break;
+ case 'popDebugGroup':
+ {
+ encoder.popDebugGroup();
+ }
+ break;
+ case 'writeTimestamp':
+ {
+ encoder.writeTimestamp(querySet, 0);
+ }
+ break;
+ case 'resolveQuerySet':
+ {
+ encoder.resolveQuerySet(querySet, 0, 1, dstBuffer, 0);
+ }
+ break;
+ default:
+ unreachable();
+ }
+ }, finishBeforeCommand);
+ });
+
+g.test('render_pass_commands')
+ .desc(
+ `
+ Test that functions of GPURenderPassEncoder generate a validation error if the encoder or the
+ pass is already finished.
+
+ - TODO: Consider testing: nothing before command, end before command, end+finish before command.
+ `
+ )
+ .params(u =>
+ u
+ .combine('command', kRenderPassEncoderCommands)
+ .beginSubcases()
+ .combine('finishBeforeCommand', [false, true])
+ )
+ .fn(t => {
+ const { command, finishBeforeCommand } = t.params;
+
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: 1 });
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = beginRenderPassWithQuerySet(t, encoder, querySet);
+
+ const buffer = t.device.createBuffer({
+ size: 12,
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.VERTEX,
+ });
+
+ const pipeline = t.createRenderPipelineForTest();
+
+ const bindGroup = t.createBindGroupForTest();
+
+ if (finishBeforeCommand) {
+ renderPass.end();
+ encoder.finish();
+ }
+
+ t.expectValidationError(() => {
+ switch (command) {
+ case 'draw':
+ {
+ renderPass.draw(1);
+ }
+ break;
+ case 'drawIndexed':
+ {
+ renderPass.drawIndexed(1);
+ }
+ break;
+ case 'drawIndirect':
+ {
+ renderPass.drawIndirect(buffer, 1);
+ }
+ break;
+ case 'setIndexBuffer':
+ {
+ renderPass.setIndexBuffer(buffer, 'uint32');
+ }
+ break;
+ case 'drawIndexedIndirect':
+ {
+ renderPass.drawIndexedIndirect(buffer, 0);
+ }
+ break;
+ case 'setBindGroup':
+ {
+ renderPass.setBindGroup(0, bindGroup);
+ }
+ break;
+ case 'setVertexBuffer':
+ {
+ renderPass.setVertexBuffer(1, buffer);
+ }
+ break;
+ case 'setPipeline':
+ {
+ renderPass.setPipeline(pipeline);
+ }
+ break;
+ case 'setViewport':
+ {
+ const kNumTestPoints = 8;
+ const kViewportMinDepth = 0;
+ const kViewportMaxDepth = 1;
+ renderPass.setViewport(0, 0, kNumTestPoints, 0, kViewportMinDepth, kViewportMaxDepth);
+ }
+ break;
+ case 'setScissorRect':
+ {
+ renderPass.setScissorRect(0, 0, 0, 0);
+ }
+ break;
+ case 'setBlendConstant':
+ {
+ renderPass.setBlendConstant({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
+ }
+ break;
+ case 'setStencilReference':
+ {
+ renderPass.setStencilReference(0);
+ }
+ break;
+ case 'beginOcclusionQuery':
+ {
+ renderPass.beginOcclusionQuery(0);
+ }
+ break;
+ case 'endOcclusionQuery':
+ {
+ renderPass.endOcclusionQuery();
+ }
+ break;
+ case 'executeBundles':
+ {
+ renderPass.executeBundles([]);
+ }
+ break;
+ case 'pushDebugGroup':
+ {
+ encoder.pushDebugGroup('group');
+ }
+ break;
+ case 'popDebugGroup':
+ {
+ encoder.popDebugGroup();
+ }
+ break;
+ case 'insertDebugMarker':
+ {
+ encoder.insertDebugMarker('marker');
+ }
+ break;
+ default:
+ unreachable();
+ }
+ }, finishBeforeCommand);
+ });
+
+g.test('render_bundle_commands')
+ .desc(
+ `
+ Test that functions of GPURenderBundleEncoder generate a validation error if the encoder or the
+ pass is already finished.
+ `
+ )
+ .params(u =>
+ u
+ .combine('command', kRenderBundleEncoderCommands)
+ .beginSubcases()
+ .combine('finishBeforeCommand', [false, true])
+ )
+ .fn(t => {
+ const { command, finishBeforeCommand } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 12,
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.VERTEX,
+ });
+
+ const pipeline = t.createRenderPipelineForTest();
+
+ const bindGroup = t.createBindGroupForTest();
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ });
+
+ if (finishBeforeCommand) {
+ bundleEncoder.finish();
+ }
+
+ t.expectValidationError(() => {
+ switch (command) {
+ case 'draw':
+ {
+ bundleEncoder.draw(1);
+ }
+ break;
+ case 'drawIndexed':
+ {
+ bundleEncoder.drawIndexed(1);
+ }
+ break;
+ case 'drawIndexedIndirect':
+ {
+ bundleEncoder.drawIndexedIndirect(buffer, 0);
+ }
+ break;
+ case 'drawIndirect':
+ {
+ bundleEncoder.drawIndirect(buffer, 1);
+ }
+ break;
+ case 'setPipeline':
+ {
+ bundleEncoder.setPipeline(pipeline);
+ }
+ break;
+ case 'setBindGroup':
+ {
+ bundleEncoder.setBindGroup(0, bindGroup);
+ }
+ break;
+ case 'setIndexBuffer':
+ {
+ bundleEncoder.setIndexBuffer(buffer, 'uint32');
+ }
+ break;
+ case 'setVertexBuffer':
+ {
+ bundleEncoder.setVertexBuffer(1, buffer);
+ }
+ break;
+ case 'pushDebugGroup':
+ {
+ bundleEncoder.pushDebugGroup('group');
+ }
+ break;
+ case 'popDebugGroup':
+ {
+ bundleEncoder.popDebugGroup();
+ }
+ break;
+ case 'insertDebugMarker':
+ {
+ bundleEncoder.insertDebugMarker('marker');
+ }
+ break;
+ default:
+ unreachable();
+ }
+ }, finishBeforeCommand);
+ });
+
+g.test('compute_pass_commands')
+ .desc(
+ `
+ Test that functions of GPUComputePassEncoder generate a validation error if the encoder or the
+ pass is already finished.
+
+ - TODO: Consider testing: nothing before command, end before command, end+finish before command.
+ `
+ )
+ .params(u =>
+ u
+ .combine('command', kComputePassEncoderCommands)
+ .beginSubcases()
+ .combine('finishBeforeCommand', [false, true])
+ )
+ .fn(t => {
+ const { command, finishBeforeCommand } = t.params;
+
+ const encoder = t.device.createCommandEncoder();
+ const computePass = encoder.beginComputePass();
+
+ const indirectBuffer = t.device.createBuffer({
+ size: 12,
+ usage: GPUBufferUsage.INDIRECT,
+ });
+
+ const computePipeline = t.createNoOpComputePipeline();
+
+ const bindGroup = t.createBindGroupForTest();
+
+ if (finishBeforeCommand) {
+ computePass.end();
+ encoder.finish();
+ }
+
+ t.expectValidationError(() => {
+ switch (command) {
+ case 'setBindGroup':
+ {
+ computePass.setBindGroup(0, bindGroup);
+ }
+ break;
+ case 'setPipeline':
+ {
+ computePass.setPipeline(computePipeline);
+ }
+ break;
+ case 'dispatchWorkgroups':
+ {
+ computePass.dispatchWorkgroups(0);
+ }
+ break;
+ case 'dispatchWorkgroupsIndirect':
+ {
+ computePass.dispatchWorkgroupsIndirect(indirectBuffer, 0);
+ }
+ break;
+ case 'pushDebugGroup':
+ {
+ computePass.pushDebugGroup('group');
+ }
+ break;
+ case 'popDebugGroup':
+ {
+ computePass.popDebugGroup();
+ }
+ break;
+ case 'insertDebugMarker':
+ {
+ computePass.insertDebugMarker('marker');
+ }
+ break;
+ default:
+ unreachable();
+ }
+ }, finishBeforeCommand);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_state.spec.ts
new file mode 100644
index 0000000000..ac3f1ef553
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/encoder_state.spec.ts
@@ -0,0 +1,204 @@
+export const description = `
+TODO:
+- createCommandEncoder
+- non-pass command, or beginPass, during {render, compute} pass
+- {before (control case), after} finish()
+ - x= {finish(), ... all non-pass commands}
+- {before (control case), after} end()
+ - x= {render, compute} pass
+ - x= {finish(), ... all relevant pass commands}
+ - x= {
+ - before endPass (control case)
+ - after endPass (no pass open)
+ - after endPass+beginPass (a new pass of the same type is open)
+ - }
+ - should make whole encoder invalid
+- ?
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { objectEquals } from '../../../../common/util/util.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ beginRenderPass(commandEncoder: GPUCommandEncoder, view: GPUTextureView): GPURenderPassEncoder {
+ return commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+
+ createAttachmentTextureView(): GPUTextureView {
+ const texture = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ this.trackForCleanup(texture);
+ return texture.createView();
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('pass_end_invalid_order')
+ .desc(
+ `
+ Test that beginning a {compute,render} pass before ending the previous {compute,render} pass
+ causes an error.
+
+ TODO: Update this test according to https://github.com/gpuweb/gpuweb/issues/2464
+ `
+ )
+ .params(u =>
+ u
+ .combine('pass0Type', ['compute', 'render'])
+ .combine('pass1Type', ['compute', 'render'])
+ .beginSubcases()
+ .combine('firstPassEnd', [true, false])
+ .combine('endPasses', [[], [0], [1], [0, 1], [1, 0]])
+ )
+ .fn(async t => {
+ const { pass0Type, pass1Type, firstPassEnd, endPasses } = t.params;
+
+ const view = t.createAttachmentTextureView();
+ const encoder = t.device.createCommandEncoder();
+
+ const firstPass =
+ pass0Type === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder, view);
+
+ if (firstPassEnd) firstPass.end();
+
+ // Begin a second pass before ending the previous pass.
+ const secondPass =
+ pass1Type === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder, view);
+
+ const passes = [firstPass, secondPass];
+ for (const index of endPasses) {
+ passes[index].end();
+ }
+
+ // If {endPasses} is '[1]' and {firstPass} ends, it's a control case.
+ const valid = firstPassEnd && objectEquals(endPasses, [1]);
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !valid);
+ });
+
+g.test('call_after_successful_finish')
+ .desc(`Test that encoding command after a successful finish generates a validation error.`)
+ .params(u =>
+ u
+ .combine('callCmd', ['beginComputePass', 'beginRenderPass', 'insertDebugMarker'])
+ .beginSubcases()
+ .combine('prePassType', ['compute', 'render', 'no-op'])
+ .combine('IsEncoderFinished', [false, true])
+ )
+ .fn(async t => {
+ const { prePassType, IsEncoderFinished, callCmd } = t.params;
+
+ const view = t.createAttachmentTextureView();
+ const encoder = t.device.createCommandEncoder();
+
+ if (prePassType !== 'no-op') {
+ const pass =
+ prePassType === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder, view);
+ pass.end();
+ }
+
+ if (IsEncoderFinished) {
+ encoder.finish();
+ }
+
+ switch (callCmd) {
+ case 'beginComputePass':
+ {
+ let pass: GPUComputePassEncoder;
+ t.expectValidationError(() => {
+ pass = encoder.beginComputePass();
+ }, IsEncoderFinished);
+ t.expectValidationError(() => {
+ pass.end();
+ }, IsEncoderFinished);
+ }
+ break;
+ case 'beginRenderPass':
+ {
+ let pass: GPURenderPassEncoder;
+ t.expectValidationError(() => {
+ pass = t.beginRenderPass(encoder, view);
+ }, IsEncoderFinished);
+ t.expectValidationError(() => {
+ pass.end();
+ }, IsEncoderFinished);
+ }
+ break;
+ case 'insertDebugMarker':
+ t.expectValidationError(() => {
+ encoder.insertDebugMarker('');
+ }, IsEncoderFinished);
+ break;
+ }
+
+ if (!IsEncoderFinished) {
+ encoder.finish();
+ }
+ });
+
+g.test('pass_end_none')
+ .desc(
+ `
+ Test that ending a {compute,render} pass without ending the passes generates a validation error.
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('passType', ['compute', 'render']).combine('endCount', [0, 1]))
+ .fn(async t => {
+ const { passType, endCount } = t.params;
+
+ const view = t.createAttachmentTextureView();
+ const encoder = t.device.createCommandEncoder();
+
+ const pass =
+ passType === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder, view);
+
+ for (let i = 0; i < endCount; ++i) {
+ pass.end();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, endCount === 0);
+ });
+
+g.test('pass_end_twice')
+ .desc('Test that ending a {compute,render} pass twice generates a validation error.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('passType', ['compute', 'render'])
+ .combine('endTwice', [false, true])
+ )
+ .fn(async t => {
+ const { passType, endTwice } = t.params;
+
+ const view = t.createAttachmentTextureView();
+ const encoder = t.device.createCommandEncoder();
+
+ const pass =
+ passType === 'compute' ? encoder.beginComputePass() : t.beginRenderPass(encoder, view);
+
+ pass.end();
+ if (endTwice) {
+ t.expectValidationError(() => {
+ pass.end();
+ });
+ }
+
+ encoder.finish();
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts
new file mode 100644
index 0000000000..a7b292bed0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts
@@ -0,0 +1,790 @@
+export const description = `
+TODO:
+- test compatibility between bind groups and pipelines
+ - the binding resource in bindGroups[i].layout is "group-equivalent" (value-equal) to pipelineLayout.bgls[i].
+ - in the test fn, test once without the dispatch/draw (should always be valid) and once with
+ the dispatch/draw, to make sure the validation happens in dispatch/draw.
+ - x= {dispatch, all draws} (dispatch/draw should be size 0 to make sure validation still happens if no-op)
+ - x= all relevant stages
+
+TODO: subsume existing test, rewrite fixture as needed.
+TODO: Add externalTexture to kResourceTypes [1]
+`;
+
+import { kUnitCaseParamsBuilder } from '../../../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { memcpy, unreachable } from '../../../../../common/util/util.js';
+import {
+ kSamplerBindingTypes,
+ kShaderStageCombinations,
+ kBufferBindingTypes,
+ ValidBindableResource,
+} from '../../../../capability_info.js';
+import { GPUConst } from '../../../../constants.js';
+import {
+ ProgrammableEncoderType,
+ kProgrammableEncoderTypes,
+} from '../../../../util/command_buffer_maker.js';
+import { ValidationTest } from '../../validation_test.js';
+
+const kComputeCmds = ['dispatch', 'dispatchIndirect'] as const;
+type ComputeCmd = typeof kComputeCmds[number];
+const kRenderCmds = ['draw', 'drawIndexed', 'drawIndirect', 'drawIndexedIndirect'] as const;
+type RenderCmd = typeof kRenderCmds[number];
+
+// Test resource type compatibility in pipeline and bind group
+// [1]: Need to add externalTexture
+const kResourceTypes: ValidBindableResource[] = [
+ 'uniformBuf',
+ 'filtSamp',
+ 'sampledTex',
+ 'storageTex',
+];
+
+function getTestCmds(
+ encoderType: ProgrammableEncoderType
+): readonly ComputeCmd[] | readonly RenderCmd[] {
+ return encoderType === 'compute pass' ? kComputeCmds : kRenderCmds;
+}
+
+const kCompatTestParams = kUnitCaseParamsBuilder
+ .combine('encoderType', kProgrammableEncoderTypes)
+ .expand('call', p => getTestCmds(p.encoderType))
+ .combine('callWithZero', [true, false]);
+
+class F extends ValidationTest {
+ getIndexBuffer(): GPUBuffer {
+ return this.device.createBuffer({
+ size: 8 * Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.INDEX,
+ });
+ }
+
+ getIndirectBuffer(indirectParams: Array<number>): GPUBuffer {
+ const buffer = this.device.createBuffer({
+ mappedAtCreation: true,
+ size: indirectParams.length * Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
+ });
+ memcpy({ src: new Uint32Array(indirectParams) }, { dst: buffer.getMappedRange() });
+ buffer.unmap();
+ return buffer;
+ }
+
+ getBindingResourceType(entry: GPUBindGroupLayoutEntry): ValidBindableResource {
+ if (entry.buffer !== undefined) return 'uniformBuf';
+ if (entry.sampler !== undefined) return 'filtSamp';
+ if (entry.texture !== undefined) return 'sampledTex';
+ if (entry.storageTexture !== undefined) return 'storageTex';
+ unreachable();
+ }
+
+ createRenderPipelineWithLayout(
+ bindGroups: Array<Array<GPUBindGroupLayoutEntry>>
+ ): GPURenderPipeline {
+ const shader = `
+ @vertex fn vs_main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 0.0, 1.0);
+ }
+
+ @fragment fn fs_main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }
+ `;
+ const module = this.device.createShaderModule({ code: shader });
+ const pipeline = this.device.createRenderPipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: bindGroups.map(entries => this.device.createBindGroupLayout({ entries })),
+ }),
+ vertex: {
+ module,
+ entryPoint: 'vs_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'fs_main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ return pipeline;
+ }
+
+ createComputePipelineWithLayout(
+ bindGroups: Array<Array<GPUBindGroupLayoutEntry>>
+ ): GPUComputePipeline {
+ const shader = `
+ @compute @workgroup_size(1)
+ fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ }
+ `;
+
+ const module = this.device.createShaderModule({ code: shader });
+ const pipeline = this.device.createComputePipeline({
+ layout: this.device.createPipelineLayout({
+ bindGroupLayouts: bindGroups.map(entries => this.device.createBindGroupLayout({ entries })),
+ }),
+ compute: {
+ module,
+ entryPoint: 'main',
+ },
+ });
+ return pipeline;
+ }
+
+ createBindGroupWithLayout(bglEntries: Array<GPUBindGroupLayoutEntry>): GPUBindGroup {
+ const bgEntries: Array<GPUBindGroupEntry> = [];
+ for (const entry of bglEntries) {
+ const resource = this.getBindingResource(this.getBindingResourceType(entry));
+ bgEntries.push({
+ binding: entry.binding,
+ resource,
+ });
+ }
+
+ return this.device.createBindGroup({
+ entries: bgEntries,
+ layout: this.device.createBindGroupLayout({ entries: bglEntries }),
+ });
+ }
+
+ doCompute(pass: GPUComputePassEncoder, call: ComputeCmd | undefined, callWithZero: boolean) {
+ const x = callWithZero ? 0 : 1;
+ switch (call) {
+ case 'dispatch':
+ pass.dispatchWorkgroups(x, 1, 1);
+ break;
+ case 'dispatchIndirect':
+ pass.dispatchWorkgroupsIndirect(this.getIndirectBuffer([x, 1, 1]), 0);
+ break;
+ default:
+ break;
+ }
+ }
+
+ doRender(
+ pass: GPURenderPassEncoder | GPURenderBundleEncoder,
+ call: RenderCmd | undefined,
+ callWithZero: boolean
+ ) {
+ const vertexCount = callWithZero ? 0 : 3;
+ switch (call) {
+ case 'draw':
+ pass.draw(vertexCount, 1, 0, 0);
+ break;
+ case 'drawIndexed':
+ pass.setIndexBuffer(this.getIndexBuffer(), 'uint32');
+ pass.drawIndexed(vertexCount, 1, 0, 0, 0);
+ break;
+ case 'drawIndirect':
+ pass.drawIndirect(this.getIndirectBuffer([vertexCount, 1, 0, 0, 0]), 0);
+ break;
+ case 'drawIndexedIndirect':
+ pass.setIndexBuffer(this.getIndexBuffer(), 'uint32');
+ pass.drawIndexedIndirect(this.getIndirectBuffer([vertexCount, 1, 0, 0, 0]), 0);
+ break;
+ default:
+ break;
+ }
+ }
+
+ createBindGroupLayoutEntry(
+ encoderType: ProgrammableEncoderType,
+ resourceType: ValidBindableResource,
+ useU32Array: boolean
+ ): GPUBindGroupLayoutEntry {
+ const entry: GPUBindGroupLayoutEntry = {
+ binding: 0,
+ visibility: encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.FRAGMENT,
+ };
+
+ switch (resourceType) {
+ case 'uniformBuf':
+ entry.buffer = { hasDynamicOffset: useU32Array }; // default type: uniform
+ break;
+ case 'filtSamp':
+ entry.sampler = {}; // default type: filtering
+ break;
+ case 'sampledTex':
+ entry.texture = {}; // default sampleType: float
+ break;
+ case 'storageTex':
+ entry.storageTexture = { access: 'write-only', format: 'rgba8unorm' };
+ break;
+ }
+
+ return entry;
+ }
+
+ runTest(
+ encoderType: ProgrammableEncoderType,
+ pipeline: GPUComputePipeline | GPURenderPipeline,
+ bindGroups: Array<GPUBindGroup | undefined>,
+ dynamicOffsets: Array<number> | undefined,
+ call: ComputeCmd | RenderCmd | undefined,
+ callWithZero: boolean,
+ success: boolean
+ ) {
+ const { encoder, validateFinish } = this.createEncoder(encoderType);
+
+ if (encoder instanceof GPUComputePassEncoder) {
+ encoder.setPipeline(pipeline as GPUComputePipeline);
+ } else {
+ encoder.setPipeline(pipeline as GPURenderPipeline);
+ }
+
+ for (let i = 0; i < bindGroups.length; i++) {
+ const bindGroup = bindGroups[i];
+ if (!bindGroup) {
+ break;
+ }
+ if (dynamicOffsets) {
+ encoder.setBindGroup(
+ i,
+ bindGroup,
+ new Uint32Array(dynamicOffsets),
+ 0,
+ dynamicOffsets.length
+ );
+ } else {
+ encoder.setBindGroup(i, bindGroup);
+ }
+ }
+
+ if (encoder instanceof GPUComputePassEncoder) {
+ this.doCompute(encoder, call as ComputeCmd, callWithZero);
+ } else {
+ this.doRender(encoder, call as RenderCmd, callWithZero);
+ }
+
+ validateFinish(success);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('bind_groups_and_pipeline_layout_mismatch')
+ .desc(
+ `
+ Tests the bind groups must match the requirements of the pipeline layout.
+ - bind groups required by the pipeline layout are required.
+ - bind groups unused by the pipeline layout can be set or not.
+ `
+ )
+ .params(
+ kCompatTestParams
+ .beginSubcases()
+ .combineWithParams([
+ { setBindGroup0: true, setBindGroup1: true, setUnusedBindGroup2: true, _success: true },
+ { setBindGroup0: true, setBindGroup1: true, setUnusedBindGroup2: false, _success: true },
+ { setBindGroup0: true, setBindGroup1: false, setUnusedBindGroup2: true, _success: false },
+ { setBindGroup0: false, setBindGroup1: true, setUnusedBindGroup2: true, _success: false },
+ { setBindGroup0: false, setBindGroup1: false, setUnusedBindGroup2: false, _success: false },
+ ])
+ .combine('useU32Array', [false, true])
+ )
+ .fn(t => {
+ const {
+ encoderType,
+ call,
+ callWithZero,
+ setBindGroup0,
+ setBindGroup1,
+ setUnusedBindGroup2,
+ _success,
+ useU32Array,
+ } = t.params;
+ const visibility =
+ encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.VERTEX;
+
+ const bindGroupLayouts: Array<Array<GPUBindGroupLayoutEntry>> = [
+ // bind group layout 0
+ [
+ {
+ binding: 0,
+ visibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ },
+ ],
+ // bind group layout 1
+ [
+ {
+ binding: 0,
+ visibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ },
+ ],
+ ];
+
+ // Create required bind groups
+ const bindGroup0 = setBindGroup0 ? t.createBindGroupWithLayout(bindGroupLayouts[0]) : undefined;
+ const bindGroup1 = setBindGroup1 ? t.createBindGroupWithLayout(bindGroupLayouts[1]) : undefined;
+ const unusedBindGroup2 = setUnusedBindGroup2
+ ? t.createBindGroupWithLayout(bindGroupLayouts[1])
+ : undefined;
+
+ // Create fixed pipeline
+ const pipeline =
+ encoderType === 'compute pass'
+ ? t.createComputePipelineWithLayout(bindGroupLayouts)
+ : t.createRenderPipelineWithLayout(bindGroupLayouts);
+
+ const dynamicOffsets = useU32Array ? [0] : undefined;
+
+ // Test without the dispatch/draw (should always be valid)
+ t.runTest(
+ encoderType,
+ pipeline,
+ [bindGroup0, bindGroup1, unusedBindGroup2],
+ dynamicOffsets,
+ undefined,
+ false,
+ true
+ );
+
+ // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw.
+ t.runTest(
+ encoderType,
+ pipeline,
+ [bindGroup0, bindGroup1, unusedBindGroup2],
+ dynamicOffsets,
+ call,
+ callWithZero,
+ _success
+ );
+ });
+
+g.test('buffer_binding,render_pipeline')
+ .desc(
+ `
+ The GPUBufferBindingLayout bindings configure should be exactly
+ same in PipelineLayout and bindgroup.
+ - TODO: test more draw functions, e.g. indirect
+ - TODO: test more visibilities, e.g. vertex
+ - TODO: bind group should be created with different layout
+ `
+ )
+ .params(u => u.combine('type', kBufferBindingTypes))
+ .fn(async t => {
+ const { type } = t.params;
+
+ // Create fixed bindGroup
+ const uniformBuffer = t.getUniformBuffer();
+
+ const bindGroup = t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ },
+ },
+ ],
+ layout: t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: {}, // default type: uniform
+ },
+ ],
+ }),
+ });
+
+ // Create pipeline with different layouts
+ const pipeline = t.createRenderPipelineWithLayout([
+ [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: {
+ type,
+ },
+ },
+ ],
+ ]);
+
+ const { encoder, validateFinish } = t.createEncoder('render pass');
+ encoder.setPipeline(pipeline);
+ encoder.setBindGroup(0, bindGroup);
+ encoder.draw(3);
+
+ validateFinish(type === undefined || type === 'uniform');
+ });
+
+g.test('sampler_binding,render_pipeline')
+ .desc(
+ `
+ The GPUSamplerBindingLayout bindings configure should be exactly
+ same in PipelineLayout and bindgroup.
+ - TODO: test more draw functions, e.g. indirect
+ - TODO: test more visibilities, e.g. vertex
+ `
+ )
+ .params(u =>
+ u //
+ .combine('bglType', kSamplerBindingTypes)
+ .combine('bgType', kSamplerBindingTypes)
+ )
+ .fn(async t => {
+ const { bglType, bgType } = t.params;
+ const bindGroup = t.device.createBindGroup({
+ entries: [
+ {
+ binding: 0,
+ resource:
+ bgType === 'comparison'
+ ? t.device.createSampler({ compare: 'always' })
+ : t.device.createSampler(),
+ },
+ ],
+ layout: t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ sampler: { type: bgType },
+ },
+ ],
+ }),
+ });
+
+ // Create pipeline with different layouts
+ const pipeline = t.createRenderPipelineWithLayout([
+ [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ sampler: {
+ type: bglType,
+ },
+ },
+ ],
+ ]);
+
+ const { encoder, validateFinish } = t.createEncoder('render pass');
+ encoder.setPipeline(pipeline);
+ encoder.setBindGroup(0, bindGroup);
+ encoder.draw(3);
+
+ validateFinish(bglType === bgType);
+ });
+
+g.test('bgl_binding_mismatch')
+ .desc(
+ 'Tests the binding number must exist or not exist in both bindGroups[i].layout and pipelineLayout.bgls[i]'
+ )
+ .params(
+ kCompatTestParams
+ .beginSubcases()
+ .combineWithParams([
+ { bgBindings: [0, 1, 2], plBindings: [0, 1, 2], _success: true },
+ { bgBindings: [0, 1, 2], plBindings: [0, 1, 3], _success: false },
+ { bgBindings: [0, 2], plBindings: [0, 2], _success: true },
+ { bgBindings: [0, 2], plBindings: [2, 0], _success: true },
+ { bgBindings: [0, 1, 2], plBindings: [0, 1], _success: false },
+ { bgBindings: [0, 1], plBindings: [0, 1, 2], _success: false },
+ ])
+ .combine('useU32Array', [false, true])
+ )
+ .fn(t => {
+ const {
+ encoderType,
+ call,
+ callWithZero,
+ bgBindings,
+ plBindings,
+ _success,
+ useU32Array,
+ } = t.params;
+ const visibility =
+ encoderType === 'compute pass' ? GPUShaderStage.COMPUTE : GPUShaderStage.VERTEX;
+
+ const bglEntries: Array<GPUBindGroupLayoutEntry> = [];
+ for (const binding of bgBindings) {
+ bglEntries.push({
+ binding,
+ visibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ });
+ }
+ const bindGroup = t.createBindGroupWithLayout(bglEntries);
+
+ const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [[]];
+ for (const binding of plBindings) {
+ plEntries[0].push({
+ binding,
+ visibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ });
+ }
+ const pipeline =
+ encoderType === 'compute pass'
+ ? t.createComputePipelineWithLayout(plEntries)
+ : t.createRenderPipelineWithLayout(plEntries);
+
+ const dynamicOffsets = useU32Array ? new Array(bgBindings.length).fill(0) : undefined;
+
+ // Test without the dispatch/draw (should always be valid)
+ t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true);
+
+ // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw.
+ t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, call, callWithZero, _success);
+ });
+
+g.test('bgl_visibility_mismatch')
+ .desc('Tests the visibility in bindGroups[i].layout and pipelineLayout.bgls[i] must be matched')
+ .params(
+ kCompatTestParams
+ .beginSubcases()
+ .combine('bgVisibility', kShaderStageCombinations)
+ .expand('plVisibility', p =>
+ p.encoderType === 'compute pass'
+ ? ([GPUConst.ShaderStage.COMPUTE] as const)
+ : ([
+ GPUConst.ShaderStage.VERTEX,
+ GPUConst.ShaderStage.FRAGMENT,
+ GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT,
+ ] as const)
+ )
+ .combine('useU32Array', [false, true])
+ )
+ .fn(t => {
+ const { encoderType, call, callWithZero, bgVisibility, plVisibility, useU32Array } = t.params;
+
+ const bglEntries: Array<GPUBindGroupLayoutEntry> = [
+ {
+ binding: 0,
+ visibility: bgVisibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ },
+ ];
+ const bindGroup = t.createBindGroupWithLayout(bglEntries);
+
+ const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [
+ [
+ {
+ binding: 0,
+ visibility: plVisibility,
+ buffer: { hasDynamicOffset: useU32Array }, // default type: uniform
+ },
+ ],
+ ];
+ const pipeline =
+ encoderType === 'compute pass'
+ ? t.createComputePipelineWithLayout(plEntries)
+ : t.createRenderPipelineWithLayout(plEntries);
+
+ const dynamicOffsets = useU32Array ? [0] : undefined;
+
+ // Test without the dispatch/draw (should always be valid)
+ t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true);
+
+ // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw.
+ t.runTest(
+ encoderType,
+ pipeline,
+ [bindGroup],
+ dynamicOffsets,
+ call,
+ callWithZero,
+ bgVisibility === plVisibility
+ );
+ });
+
+g.test('bgl_resource_type_mismatch')
+ .desc(
+ `
+ Tests the binding resource type in bindGroups[i].layout and pipelineLayout.bgls[i] must be matched
+ - TODO: Test externalTexture
+ `
+ )
+ .params(
+ kCompatTestParams
+ .beginSubcases()
+ .combine('bgResourceType', kResourceTypes)
+ .combine('plResourceType', kResourceTypes)
+ .expand('useU32Array', p => (p.bgResourceType === 'uniformBuf' ? [true, false] : [false]))
+ )
+ .fn(t => {
+ const {
+ encoderType,
+ call,
+ callWithZero,
+ bgResourceType,
+ plResourceType,
+ useU32Array,
+ } = t.params;
+
+ const bglEntries: Array<GPUBindGroupLayoutEntry> = [
+ t.createBindGroupLayoutEntry(encoderType, bgResourceType, useU32Array),
+ ];
+ const bindGroup = t.createBindGroupWithLayout(bglEntries);
+
+ const plEntries: Array<Array<GPUBindGroupLayoutEntry>> = [
+ [t.createBindGroupLayoutEntry(encoderType, plResourceType, useU32Array)],
+ ];
+ const pipeline =
+ encoderType === 'compute pass'
+ ? t.createComputePipelineWithLayout(plEntries)
+ : t.createRenderPipelineWithLayout(plEntries);
+
+ const dynamicOffsets = useU32Array ? [0] : undefined;
+
+ // Test without the dispatch/draw (should always be valid)
+ t.runTest(encoderType, pipeline, [bindGroup], dynamicOffsets, undefined, false, true);
+
+ // Test with the dispatch/draw, to make sure the validation happens in dispatch/draw.
+ t.runTest(
+ encoderType,
+ pipeline,
+ [bindGroup],
+ dynamicOffsets,
+ call,
+ callWithZero,
+ bgResourceType === plResourceType
+ );
+ });
+
+g.test('empty_bind_group_layouts_requires_empty_bind_groups,compute_pass')
+ .desc(
+ `
+ Test that a compute pipeline with empty bind groups layouts requires empty bind groups to be set.
+ `
+ )
+ .params(u =>
+ u
+ .combine('bindGroupLayoutEntryCount', [3, 4])
+ .combine('computeCommand', ['dispatchIndirect', 'dispatch'] as const)
+ )
+ .fn(async t => {
+ const { bindGroupLayoutEntryCount, computeCommand } = t.params;
+
+ const emptyBGLCount = 4;
+ const emptyBGL = t.device.createBindGroupLayout({ entries: [] });
+ const emptyBGLs = [];
+ for (let i = 0; i < emptyBGLCount; i++) {
+ emptyBGLs.push(emptyBGL);
+ }
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: emptyBGLs,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: t.device.createShaderModule({
+ code: '@compute @workgroup_size(1) fn main() {}',
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const emptyBindGroup = t.device.createBindGroup({
+ layout: emptyBGL,
+ entries: [],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const computePass = encoder.beginComputePass();
+ computePass.setPipeline(pipeline);
+ for (let i = 0; i < bindGroupLayoutEntryCount; i++) {
+ computePass.setBindGroup(i, emptyBindGroup);
+ }
+
+ t.doCompute(computePass, computeCommand, true);
+ computePass.end();
+
+ const success = bindGroupLayoutEntryCount === emptyBGLCount;
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('empty_bind_group_layouts_requires_empty_bind_groups,render_pass')
+ .desc(
+ `
+ Test that a render pipeline with empty bind groups layouts requires empty bind groups to be set.
+ `
+ )
+ .params(u =>
+ u
+ .combine('bindGroupLayoutEntryCount', [3, 4])
+ .combine('renderCommand', [
+ 'draw',
+ 'drawIndexed',
+ 'drawIndirect',
+ 'drawIndexedIndirect',
+ ] as const)
+ )
+ .fn(async t => {
+ const { bindGroupLayoutEntryCount, renderCommand } = t.params;
+
+ const emptyBGLCount = 4;
+ const emptyBGL = t.device.createBindGroupLayout({ entries: [] });
+ const emptyBGLs = [];
+ for (let i = 0; i < emptyBGLCount; i++) {
+ emptyBGLs.push(emptyBGL);
+ }
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: emptyBGLs,
+ });
+
+ const colorFormat = 'rgba8unorm';
+ const pipeline = t.device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() {}`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: colorFormat, writeMask: 0 }],
+ },
+ });
+
+ const emptyBindGroup = t.device.createBindGroup({
+ layout: emptyBGL,
+ entries: [],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+
+ const attachmentTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: attachmentTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+
+ renderPass.setPipeline(pipeline);
+ for (let i = 0; i < bindGroupLayoutEntryCount; i++) {
+ renderPass.setBindGroup(i, emptyBindGroup);
+ }
+ t.doRender(renderPass, renderCommand, true);
+ renderPass.end();
+
+ const success = bindGroupLayoutEntryCount === emptyBGLCount;
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/begin_end.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/begin_end.spec.ts
new file mode 100644
index 0000000000..580675e118
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/begin_end.spec.ts
@@ -0,0 +1,162 @@
+export const description = `
+Validation for encoding begin/endable queries.
+
+TODO: pipeline statistics queries are removed from core; consider moving tests to another suite.
+TODO: tests for pipeline statistics queries:
+- balance: {
+ - begin 0, end 1
+ - begin 1, end 0
+ - begin 1, end 1
+ - begin 2, end 2
+ - }
+ - x= {
+ - render pass + pipeline statistics
+ - compute pass + pipeline statistics
+ - }
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+import { beginRenderPassWithQuerySet, createQuerySetWithType } from './common.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('occlusion_query,begin_end_balance')
+ .desc(
+ `
+Tests that begin/end occlusion queries mismatch on render pass:
+- begin n queries, then end m queries, for various n and m.
+ `
+ )
+ .paramsSubcasesOnly([
+ { begin: 0, end: 1 },
+ { begin: 1, end: 0 },
+ { begin: 1, end: 1 }, // control case
+ { begin: 1, end: 2 },
+ { begin: 2, end: 1 },
+ ] as const)
+ .fn(async t => {
+ const { begin, end } = t.params;
+
+ const occlusionQuerySet = createQuerySetWithType(t, 'occlusion', 2);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ for (let i = 0; i < begin; i++) {
+ encoder.encoder.beginOcclusionQuery(i);
+ }
+ for (let j = 0; j < end; j++) {
+ encoder.encoder.endOcclusionQuery();
+ }
+ encoder.validateFinishAndSubmit(begin === end, true);
+ });
+
+g.test('occlusion_query,begin_end_invalid_nesting')
+ .desc(
+ `
+Tests the invalid nesting of begin/end occlusion queries:
+- begin index 0, end, begin index 0, end (control case)
+- begin index 0, begin index 0, end, end
+- begin index 0, begin index 1, end, end
+ `
+ )
+ .paramsSubcasesOnly([
+ { calls: [0, 'end', 1, 'end'], _valid: true }, // control case
+ { calls: [0, 0, 'end', 'end'], _valid: false },
+ { calls: [0, 1, 'end', 'end'], _valid: false },
+ ] as const)
+ .fn(async t => {
+ const { calls, _valid } = t.params;
+
+ const occlusionQuerySet = createQuerySetWithType(t, 'occlusion', 2);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ for (const i of calls) {
+ if (i !== 'end') {
+ encoder.encoder.beginOcclusionQuery(i);
+ } else {
+ encoder.encoder.endOcclusionQuery();
+ }
+ }
+ encoder.validateFinishAndSubmit(_valid, true);
+ });
+
+g.test('occlusion_query,disjoint_queries_with_same_query_index')
+ .desc(
+ `
+Tests that two disjoint occlusion queries cannot be begun with same query index on same render pass:
+- begin index 0, end, begin index 0, end
+- call on {same (invalid), different (control case)} render pass
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('isOnSameRenderPass', [false, true]))
+ .fn(async t => {
+ const querySet = createQuerySetWithType(t, 'occlusion', 1);
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = beginRenderPassWithQuerySet(t, encoder, querySet);
+ pass.beginOcclusionQuery(0);
+ pass.endOcclusionQuery();
+
+ if (t.params.isOnSameRenderPass) {
+ pass.beginOcclusionQuery(0);
+ pass.endOcclusionQuery();
+ pass.end();
+ } else {
+ pass.end();
+ const otherPass = beginRenderPassWithQuerySet(t, encoder, querySet);
+ otherPass.beginOcclusionQuery(0);
+ otherPass.endOcclusionQuery();
+ otherPass.end();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, t.params.isOnSameRenderPass);
+ });
+
+g.test('nesting')
+ .desc(
+ `
+Tests that whether it's allowed to nest various types of queries:
+- call {occlusion, pipeline-statistics, timestamp} query in same type or other type.
+ `
+ )
+ .paramsSubcasesOnly([
+ { begin: 'occlusion', nest: 'timestamp', end: 'occlusion', _valid: true },
+ { begin: 'occlusion', nest: 'occlusion', end: 'occlusion', _valid: false },
+ { begin: 'occlusion', nest: 'pipeline-statistics', end: 'occlusion', _valid: true },
+ {
+ begin: 'occlusion',
+ nest: 'pipeline-statistics',
+ end: 'pipeline-statistics',
+ _valid: true,
+ },
+ {
+ begin: 'pipeline-statistics',
+ nest: 'timestamp',
+ end: 'pipeline-statistics',
+ _valid: true,
+ },
+ {
+ begin: 'pipeline-statistics',
+ nest: 'pipeline-statistics',
+ end: 'pipeline-statistics',
+ _valid: false,
+ },
+ {
+ begin: 'pipeline-statistics',
+ nest: 'occlusion',
+ end: 'pipeline-statistics',
+ _valid: true,
+ },
+ { begin: 'pipeline-statistics', nest: 'occlusion', end: 'occlusion', _valid: true },
+ { begin: 'timestamp', nest: 'occlusion', end: 'occlusion', _valid: true },
+ {
+ begin: 'timestamp',
+ nest: 'pipeline-statistics',
+ end: 'pipeline-statistics',
+ _valid: true,
+ },
+ ] as const)
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/common.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/common.ts
new file mode 100644
index 0000000000..66e8e78b13
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/common.ts
@@ -0,0 +1,37 @@
+import { GPUTest } from '../../../../gpu_test.js';
+
+export function createQuerySetWithType(
+ t: GPUTest,
+ type: GPUQueryType,
+ count: GPUSize32
+): GPUQuerySet {
+ return t.device.createQuerySet({
+ type,
+ count,
+ });
+}
+
+export function beginRenderPassWithQuerySet(
+ t: GPUTest,
+ encoder: GPUCommandEncoder,
+ querySet?: GPUQuerySet
+): GPURenderPassEncoder {
+ const view = t.device
+ .createTexture({
+ format: 'rgba8unorm' as const,
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ })
+ .createView();
+ return encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ occlusionQuerySet: querySet,
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts
new file mode 100644
index 0000000000..5dc0d70ae2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/general.spec.ts
@@ -0,0 +1,157 @@
+export const description = `
+TODO: pipeline statistics queries are removed from core; consider moving tests to another suite.
+TODO:
+- Start a pipeline statistics query in all possible encoders:
+ - queryIndex {in, out of} range for GPUQuerySet
+ - GPUQuerySet {valid, invalid, device mismatched}
+ - x ={render pass, compute pass} encoder
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { kQueryTypes } from '../../../../capability_info.js';
+import { ValidationTest } from '../../validation_test.js';
+
+import { createQuerySetWithType } from './common.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('occlusion_query,query_type')
+ .desc(
+ `
+Tests that set occlusion query set with all types in render pass descriptor:
+- type {occlusion (control case), pipeline statistics, timestamp}
+- {undefined} for occlusion query set in render pass descriptor
+ `
+ )
+ .params(u => u.combine('type', [undefined, ...kQueryTypes]))
+ .beforeAllSubcases(t => {
+ const { type } = t.params;
+ if (type) {
+ t.selectDeviceForQueryTypeOrSkipTestCase(type);
+ }
+ })
+ .fn(async t => {
+ const type = t.params.type;
+ const querySet = type === undefined ? undefined : createQuerySetWithType(t, type, 1);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet: querySet });
+ encoder.encoder.beginOcclusionQuery(0);
+ encoder.encoder.endOcclusionQuery();
+ encoder.validateFinish(type === 'occlusion');
+ });
+
+g.test('occlusion_query,invalid_query_set')
+ .desc(
+ `
+Tests that begin occlusion query with a invalid query set that failed during creation.
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('querySetState', ['valid', 'invalid'] as const))
+ .fn(t => {
+ const occlusionQuerySet = t.createQuerySetWithState(t.params.querySetState);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ encoder.encoder.beginOcclusionQuery(0);
+ encoder.encoder.endOcclusionQuery();
+ encoder.validateFinishAndSubmitGivenState(t.params.querySetState);
+ });
+
+g.test('occlusion_query,query_index')
+ .desc(
+ `
+Tests that begin occlusion query with query index:
+- queryIndex {in, out of} range for GPUQuerySet
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('queryIndex', [0, 2]))
+ .fn(t => {
+ const occlusionQuerySet = createQuerySetWithType(t, 'occlusion', 2);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ encoder.encoder.beginOcclusionQuery(t.params.queryIndex);
+ encoder.encoder.endOcclusionQuery();
+ encoder.validateFinish(t.params.queryIndex < 2);
+ });
+
+g.test('timestamp_query,query_type_and_index')
+ .desc(
+ `
+Tests that write timestamp to all types of query set on all possible encoders:
+- type {occlusion, pipeline statistics, timestamp}
+- queryIndex {in, out of} range for GPUQuerySet
+- x= {non-pass} encoder
+ `
+ )
+ .params(u =>
+ u
+ .combine('type', kQueryTypes)
+ .beginSubcases()
+ .expand('queryIndex', p => (p.type === 'timestamp' ? [0, 2] : [0]))
+ )
+ .beforeAllSubcases(t => {
+ const { type } = t.params;
+
+ // writeTimestamp is only available for devices that enable the 'timestamp-query' feature.
+ const queryTypes: GPUQueryType[] = ['timestamp'];
+ if (type !== 'timestamp') {
+ queryTypes.push(type);
+ }
+
+ t.selectDeviceForQueryTypeOrSkipTestCase(queryTypes);
+ })
+ .fn(async t => {
+ const { type, queryIndex } = t.params;
+
+ const count = 2;
+ const querySet = createQuerySetWithType(t, type, count);
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.writeTimestamp(querySet, queryIndex);
+ encoder.validateFinish(type === 'timestamp' && queryIndex < count);
+ });
+
+g.test('timestamp_query,invalid_query_set')
+ .desc(
+ `
+Tests that write timestamp to a invalid query set that failed during creation:
+- x= {non-pass} encoder
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('querySetState', ['valid', 'invalid'] as const))
+ .beforeAllSubcases(t => {
+ t.selectDeviceForQueryTypeOrSkipTestCase('timestamp');
+ })
+ .fn(async t => {
+ const { querySetState } = t.params;
+
+ const querySet = t.createQuerySetWithState(querySetState, {
+ type: 'timestamp',
+ count: 2,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.writeTimestamp(querySet, 0);
+ encoder.validateFinish(querySetState !== 'invalid');
+ });
+
+g.test('timestamp_query,device_mismatch')
+ .desc('Tests writeTimestamp cannot be called with a query set created from another device')
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceForQueryTypeOrSkipTestCase('timestamp');
+ t.selectMismatchedDeviceOrSkipTestCase('timestamp-query');
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const querySet = sourceDevice.createQuerySet({
+ type: 'timestamp',
+ count: 2,
+ });
+ t.trackForCleanup(querySet);
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.writeTimestamp(querySet, 0);
+ encoder.validateFinish(!mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/pipeline_statistics.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/pipeline_statistics.spec.ts
new file mode 100644
index 0000000000..5827f46058
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/pipeline_statistics.spec.ts
@@ -0,0 +1,14 @@
+export const description = `
+Validation for encoding pipeline statistics queries.
+Excludes query begin/end balance and nesting (begin_end.spec.ts)
+and querySet/queryIndex (general.spec.ts).
+
+TODO: pipeline statistics queries are removed from core; consider moving tests to another suite.
+TODO:
+- Test pipelineStatistics with {undefined, empty, duplicated, full (control case)} values
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/resolveQuerySet.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/resolveQuerySet.spec.ts
new file mode 100644
index 0000000000..b6c3f0f63b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/queries/resolveQuerySet.spec.ts
@@ -0,0 +1,181 @@
+export const description = `
+Validation tests for resolveQuerySet.
+`;
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../../constants.js';
+import { kResourceStates } from '../../../../gpu_test.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+export const kQueryCount = 2;
+
+g.test('queryset_and_destination_buffer_state')
+ .desc(
+ `
+Tests that resolve query set must be with valid query set and destination buffer.
+- {invalid, destroyed} GPUQuerySet results in validation error.
+- {invalid, destroyed} destination buffer results in validation error.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('querySetState', kResourceStates)
+ .combine('destinationState', kResourceStates)
+ )
+ .fn(async t => {
+ const { querySetState, destinationState } = t.params;
+
+ const shouldBeValid = querySetState !== 'invalid' && destinationState !== 'invalid';
+ const shouldSubmitSuccess = querySetState === 'valid' && destinationState === 'valid';
+
+ const querySet = t.createQuerySetWithState(querySetState);
+
+ const destination = t.createBufferWithState(destinationState, {
+ size: kQueryCount * 8,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, 1, destination, 0);
+ encoder.validateFinishAndSubmit(shouldBeValid, shouldSubmitSuccess);
+ });
+
+g.test('first_query_and_query_count')
+ .desc(
+ `
+Tests that resolve query set with invalid firstQuery and queryCount:
+- firstQuery and/or queryCount out of range
+ `
+ )
+ .paramsSubcasesOnly([
+ { firstQuery: 0, queryCount: kQueryCount }, // control case
+ { firstQuery: 0, queryCount: kQueryCount + 1 },
+ { firstQuery: 1, queryCount: kQueryCount },
+ { firstQuery: kQueryCount, queryCount: 1 },
+ ])
+ .fn(async t => {
+ const { firstQuery, queryCount } = t.params;
+
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: kQueryCount });
+ const destination = t.device.createBuffer({
+ size: kQueryCount * 8,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, firstQuery, queryCount, destination, 0);
+ encoder.validateFinish(firstQuery + queryCount <= kQueryCount);
+ });
+
+g.test('destination_buffer_usage')
+ .desc(
+ `
+Tests that resolve query set with invalid destinationBuffer:
+- Buffer usage {with, without} QUERY_RESOLVE
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('bufferUsage', [
+ GPUConst.BufferUsage.STORAGE,
+ GPUConst.BufferUsage.QUERY_RESOLVE, // control case
+ ] as const)
+ )
+ .fn(async t => {
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: kQueryCount });
+ const destination = t.device.createBuffer({
+ size: kQueryCount * 8,
+ usage: t.params.bufferUsage,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, kQueryCount, destination, 0);
+ encoder.validateFinish(t.params.bufferUsage === GPUConst.BufferUsage.QUERY_RESOLVE);
+ });
+
+g.test('destination_offset_alignment')
+ .desc(
+ `
+Tests that resolve query set with invalid destinationOffset:
+- destinationOffset is not a multiple of 256
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('destinationOffset', [0, 128, 256, 384]))
+ .fn(async t => {
+ const { destinationOffset } = t.params;
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: kQueryCount });
+ const destination = t.device.createBuffer({
+ size: 512,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, kQueryCount, destination, destinationOffset);
+ encoder.validateFinish(destinationOffset % 256 === 0);
+ });
+
+g.test('resolve_buffer_oob')
+ .desc(
+ `
+Tests that resolve query set with the size oob:
+- The size of destinationBuffer - destinationOffset < queryCount * 8
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u.combineWithParams([
+ { queryCount: 2, bufferSize: 16, destinationOffset: 0, _success: true },
+ { queryCount: 3, bufferSize: 16, destinationOffset: 0, _success: false },
+ { queryCount: 2, bufferSize: 16, destinationOffset: 256, _success: false },
+ { queryCount: 2, bufferSize: 272, destinationOffset: 256, _success: true },
+ { queryCount: 2, bufferSize: 264, destinationOffset: 256, _success: false },
+ ])
+ )
+ .fn(async t => {
+ const { queryCount, bufferSize, destinationOffset, _success } = t.params;
+ const querySet = t.device.createQuerySet({ type: 'occlusion', count: queryCount });
+ const destination = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, queryCount, destination, destinationOffset);
+ encoder.validateFinish(_success);
+ });
+
+g.test('query_set_buffer,device_mismatch')
+ .desc(
+ 'Tests resolveQuerySet cannot be called with a query set or destination buffer created from another device'
+ )
+ .paramsSubcasesOnly([
+ { querySetMismatched: false, bufferMismatched: false }, // control case
+ { querySetMismatched: true, bufferMismatched: false },
+ { querySetMismatched: false, bufferMismatched: true },
+ ] as const)
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { querySetMismatched, bufferMismatched } = t.params;
+
+ const kQueryCount = 1;
+
+ const querySetDevice = querySetMismatched ? t.mismatchedDevice : t.device;
+ const querySet = querySetDevice.createQuerySet({
+ type: 'occlusion',
+ count: kQueryCount,
+ });
+ t.trackForCleanup(querySet);
+
+ const bufferDevice = bufferMismatched ? t.mismatchedDevice : t.device;
+ const buffer = bufferDevice.createBuffer({
+ size: kQueryCount * 8,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+ t.trackForCleanup(buffer);
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, kQueryCount, buffer, 0);
+ encoder.validateFinish(!(querySetMismatched || bufferMismatched));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts
new file mode 100644
index 0000000000..90d30862c2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/encoding/render_bundle.spec.ts
@@ -0,0 +1,258 @@
+export const description = `
+Tests execution of render bundles.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kDepthStencilFormats, kTextureFormatInfo } from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('empty_bundle_list')
+ .desc(
+ `
+ Test that it is valid to execute an empty list of render bundles
+ `
+ )
+ .fn(async t => {
+ const encoder = t.createEncoder('render pass');
+ encoder.encoder.executeBundles([]);
+ encoder.validateFinish(true);
+ });
+
+g.test('device_mismatch')
+ .desc(
+ `
+ Tests executeBundles cannot be called with render bundles created from another device
+ Test with two bundles to make sure all bundles can be validated:
+ - bundle0 and bundle1 from same device
+ - bundle0 and bundle1 from different device
+ `
+ )
+ .paramsSubcasesOnly([
+ { bundle0Mismatched: false, bundle1Mismatched: false }, // control case
+ { bundle0Mismatched: true, bundle1Mismatched: false },
+ { bundle0Mismatched: false, bundle1Mismatched: true },
+ ])
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { bundle0Mismatched, bundle1Mismatched } = t.params;
+
+ const descriptor: GPURenderBundleEncoderDescriptor = {
+ colorFormats: ['rgba8unorm'],
+ };
+
+ const bundle0Device = bundle0Mismatched ? t.mismatchedDevice : t.device;
+ const bundle0 = bundle0Device.createRenderBundleEncoder(descriptor).finish();
+
+ const bundle1Device = bundle1Mismatched ? t.mismatchedDevice : t.device;
+ const bundle1 = bundle1Device.createRenderBundleEncoder(descriptor).finish();
+
+ const encoder = t.createEncoder('render pass');
+ encoder.encoder.executeBundles([bundle0, bundle1]);
+
+ encoder.validateFinish(!(bundle0Mismatched || bundle1Mismatched));
+ });
+
+g.test('color_formats_mismatch')
+ .desc(
+ `
+ Tests executeBundles cannot be called with render bundles that do match the colorFormats of the
+ render pass. This includes:
+ - formats don't match
+ - formats match but are in a different order
+ - formats match but there is a different count
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ {
+ bundleFormats: ['bgra8unorm', 'rg8unorm'] as const,
+ passFormats: ['bgra8unorm', 'rg8unorm'] as const,
+ _compatible: true,
+ }, // control case
+ {
+ bundleFormats: ['bgra8unorm', 'rg8unorm'] as const,
+ passFormats: ['bgra8unorm', 'bgra8unorm'] as const,
+ _compatible: false,
+ },
+ {
+ bundleFormats: ['bgra8unorm', 'rg8unorm'] as const,
+ passFormats: ['rg8unorm', 'bgra8unorm'] as const,
+ _compatible: false,
+ },
+ {
+ bundleFormats: ['bgra8unorm', 'rg8unorm', 'rgba8unorm'] as const,
+ passFormats: ['rg8unorm', 'bgra8unorm'] as const,
+ _compatible: false,
+ },
+ {
+ bundleFormats: ['bgra8unorm', 'rg8unorm'] as const,
+ passFormats: ['rg8unorm', 'bgra8unorm', 'rgba8unorm'] as const,
+ _compatible: false,
+ },
+ ])
+ )
+ .fn(async t => {
+ const { bundleFormats, passFormats, _compatible } = t.params;
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: bundleFormats,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const encoder = t.createEncoder('render pass', {
+ attachmentInfo: {
+ colorFormats: passFormats,
+ },
+ });
+ encoder.encoder.executeBundles([bundle]);
+
+ encoder.validateFinish(_compatible);
+ });
+
+g.test('depth_stencil_formats_mismatch')
+ .desc(
+ `
+ Tests executeBundles cannot be called with render bundles that do match the depthStencil of the
+ render pass. This includes:
+ - formats don't match
+ - formats have matching depth or stencil aspects, but other aspects are missing
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ { bundleFormat: 'depth24plus', passFormat: 'depth24plus' }, // control case
+ { bundleFormat: 'depth24plus', passFormat: 'depth16unorm' },
+ { bundleFormat: 'depth24plus', passFormat: 'depth24plus-stencil8' },
+ { bundleFormat: 'stencil8', passFormat: 'depth24plus-stencil8' },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ const { bundleFormat, passFormat } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase([bundleFormat, passFormat]);
+ })
+ .fn(async t => {
+ const { bundleFormat, passFormat } = t.params;
+ const compatible = bundleFormat === passFormat;
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: [],
+ depthStencilFormat: bundleFormat,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const encoder = t.createEncoder('render pass', {
+ attachmentInfo: {
+ colorFormats: [],
+ depthStencilFormat: passFormat,
+ },
+ });
+ encoder.encoder.executeBundles([bundle]);
+
+ encoder.validateFinish(compatible);
+ });
+
+g.test('depth_stencil_readonly_mismatch')
+ .desc(
+ `
+ Tests executeBundles cannot be called with render bundles that do match the depthStencil
+ readonly state of the render pass.
+ `
+ )
+ .params(u =>
+ u
+ .combine('depthStencilFormat', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('bundleDepthReadOnly', [false, true])
+ .combine('bundleStencilReadOnly', [false, true])
+ .combine('passDepthReadOnly', [false, true])
+ .combine('passStencilReadOnly', [false, true])
+ .filter(p => {
+ // For combined depth/stencil formats the depth and stencil read only state must match
+ // in order to create a valid render bundle or render pass.
+ const depthStencilInfo = kTextureFormatInfo[p.depthStencilFormat];
+ if (depthStencilInfo.depth && depthStencilInfo.stencil) {
+ return (
+ p.passDepthReadOnly === p.passStencilReadOnly &&
+ p.bundleDepthReadOnly === p.bundleStencilReadOnly
+ );
+ }
+ return true;
+ })
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.depthStencilFormat);
+ })
+ .fn(async t => {
+ const {
+ depthStencilFormat,
+ bundleDepthReadOnly,
+ bundleStencilReadOnly,
+ passDepthReadOnly,
+ passStencilReadOnly,
+ } = t.params;
+
+ const compatible =
+ (!passDepthReadOnly || bundleDepthReadOnly === passDepthReadOnly) &&
+ (!passStencilReadOnly || bundleStencilReadOnly === passStencilReadOnly);
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: [],
+ depthStencilFormat,
+ depthReadOnly: bundleDepthReadOnly,
+ stencilReadOnly: bundleStencilReadOnly,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const encoder = t.createEncoder('render pass', {
+ attachmentInfo: {
+ colorFormats: [],
+ depthStencilFormat,
+ depthReadOnly: passDepthReadOnly,
+ stencilReadOnly: passStencilReadOnly,
+ },
+ });
+ encoder.encoder.executeBundles([bundle]);
+
+ encoder.validateFinish(compatible);
+ });
+
+g.test('sample_count_mismatch')
+ .desc(
+ `
+ Tests executeBundles cannot be called with render bundles that do match the sampleCount of the
+ render pass.
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ { bundleSamples: 1, passSamples: 1 }, // control case
+ { bundleSamples: 4, passSamples: 4 }, // control case
+ { bundleFormat: 4, passFormat: 1 },
+ { bundleFormat: 1, passFormat: 4 },
+ ])
+ )
+ .fn(async t => {
+ const { bundleSamples, passSamples } = t.params;
+
+ const compatible = bundleSamples === passSamples;
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['bgra8unorm'],
+ sampleCount: bundleSamples,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const encoder = t.createEncoder('render pass', {
+ attachmentInfo: {
+ colorFormats: ['bgra8unorm'],
+ sampleCount: passSamples,
+ },
+ });
+ encoder.encoder.executeBundles([bundle]);
+
+ encoder.validateFinish(compatible);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts
new file mode 100644
index 0000000000..7a023f770e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/error_scope.spec.ts
@@ -0,0 +1,283 @@
+export const description = `
+Error scope validation tests.
+
+Note these must create their own device, not use GPUTest (that one already has error scopes on it).
+
+TODO: (POSTV1) Test error scopes of different threads and make sure they go to the right place.
+TODO: (POSTV1) Test that unhandled errors go the right device, and nowhere if the device was dropped.
+`;
+
+import { Fixture } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { getGPU } from '../../../common/util/navigator_gpu.js';
+import { assert, raceWithRejectOnTimeout } from '../../../common/util/util.js';
+import { kErrorScopeFilters, kGeneratableErrorScopeFilters } from '../../capability_info.js';
+import { kMaxUnsignedLongLongValue } from '../../constants.js';
+
+class ErrorScopeTests extends Fixture {
+ _device: GPUDevice | undefined = undefined;
+
+ get device(): GPUDevice {
+ assert(this._device !== undefined);
+ return this._device;
+ }
+
+ async init(): Promise<void> {
+ await super.init();
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null);
+ const device = await adapter.requestDevice();
+ assert(device !== null);
+ this._device = device;
+ }
+
+ // Generates an error of the given filter type. For now, the errors are generated by calling a
+ // known code-path to cause the error. This can be updated in the future should there be a more
+ // direct way to inject errors.
+ generateError(filter: GPUErrorFilter): void {
+ switch (filter) {
+ case 'out-of-memory':
+ // Generating an out-of-memory error by allocating a massive buffer.
+ this.device.createBuffer({
+ size: kMaxUnsignedLongLongValue, // Unrealistically massive buffer size
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ break;
+ case 'validation':
+ // Generating a validation error by passing in an invalid usage when creating a buffer.
+ this.device.createBuffer({
+ size: 1024,
+ usage: 0xffff, // Invalid GPUBufferUsage
+ });
+ break;
+ }
+ // MAINTENANCE_TODO: This is a workaround for Chromium not flushing. Remove when not needed.
+ this.device.queue.submit([]);
+ }
+
+ // Checks whether the error is of the type expected given the filter.
+ isInstanceOfError(filter: GPUErrorFilter, error: GPUError | null): boolean {
+ switch (filter) {
+ case 'out-of-memory':
+ return error instanceof GPUOutOfMemoryError;
+ case 'validation':
+ return error instanceof GPUValidationError;
+ case 'internal':
+ return error instanceof GPUInternalError;
+ }
+ }
+
+ // Expect an uncapturederror event to occur. Note: this MUST be awaited, because
+ // otherwise it could erroneously pass by capturing an error from later in the test.
+ async expectUncapturedError(fn: Function): Promise<GPUUncapturedErrorEvent> {
+ return this.immediateAsyncExpectation(() => {
+ // MAINTENANCE_TODO: Make arbitrary timeout value a test runner variable
+ const TIMEOUT_IN_MS = 1000;
+
+ const promise: Promise<GPUUncapturedErrorEvent> = new Promise(resolve => {
+ const eventListener = ((event: GPUUncapturedErrorEvent) => {
+ this.debug(`Got uncaptured error event with ${event.error}`);
+ resolve(event);
+ }) as EventListener;
+
+ this.device.addEventListener('uncapturederror', eventListener, { once: true });
+ });
+
+ fn();
+
+ return raceWithRejectOnTimeout(
+ promise,
+ TIMEOUT_IN_MS,
+ 'Timeout occurred waiting for uncaptured error'
+ );
+ });
+ }
+}
+
+export const g = makeTestGroup(ErrorScopeTests);
+
+g.test('simple')
+ .desc(
+ `
+Tests that error scopes catches their expected errors, firing an uncaptured error event otherwise.
+
+- Same error and error filter (popErrorScope should return the error)
+- Different error from filter (uncaptured error should result)
+ `
+ )
+ .params(u =>
+ u.combine('errorType', kGeneratableErrorScopeFilters).combine('errorFilter', kErrorScopeFilters)
+ )
+ .fn(async t => {
+ const { errorType, errorFilter } = t.params;
+ t.device.pushErrorScope(errorFilter);
+
+ if (errorType !== errorFilter) {
+ // Different error case
+ const uncapturedErrorEvent = await t.expectUncapturedError(() => {
+ t.generateError(errorType);
+ });
+ t.expect(t.isInstanceOfError(errorType, uncapturedErrorEvent.error));
+
+ const error = await t.device.popErrorScope();
+ t.expect(error === null);
+ } else {
+ // Same error as filter
+ t.generateError(errorType);
+ const error = await t.device.popErrorScope();
+ t.expect(t.isInstanceOfError(errorType, error));
+ }
+ });
+
+g.test('empty')
+ .desc(
+ `
+Tests that popping an empty error scope stack should reject.
+ `
+ )
+ .fn(async t => {
+ const promise = t.device.popErrorScope();
+ t.shouldReject('OperationError', promise);
+ });
+
+g.test('parent_scope')
+ .desc(
+ `
+Tests that an error bubbles to the correct parent scope.
+
+- Different error types as the parent scope
+- Different depths of non-capturing filters for the generated error
+ `
+ )
+ .params(u =>
+ u
+ .combine('errorFilter', kGeneratableErrorScopeFilters)
+ .combine('stackDepth', [1, 10, 100, 1000])
+ )
+ .fn(async t => {
+ const { errorFilter, stackDepth } = t.params;
+ t.device.pushErrorScope(errorFilter);
+
+ // Push a bunch of error filters onto the stack (none that match errorFilter)
+ const unmatchedFilters = kErrorScopeFilters.filter(filter => {
+ return filter !== errorFilter;
+ });
+ for (let i = 0; i < stackDepth; i++) {
+ t.device.pushErrorScope(unmatchedFilters[i % unmatchedFilters.length]);
+ }
+
+ // Cause the error and then pop all the unrelated filters.
+ t.generateError(errorFilter);
+ const promises = [];
+ for (let i = 0; i < stackDepth; i++) {
+ promises.push(t.device.popErrorScope());
+ }
+ const errors = await Promise.all(promises);
+ t.expect(errors.every(e => e === null));
+
+ // Finally the actual error should have been caught by the parent scope.
+ const error = await t.device.popErrorScope();
+ t.expect(t.isInstanceOfError(errorFilter, error));
+ });
+
+g.test('current_scope')
+ .desc(
+ `
+Tests that an error does not bubbles to parent scopes when local scope matches.
+
+- Different error types as the current scope
+- Different depths of non-capturing filters for the generated error
+ `
+ )
+ .params(u =>
+ u
+ .combine('errorFilter', kGeneratableErrorScopeFilters)
+ .combine('stackDepth', [1, 10, 100, 1000, 100000])
+ )
+ .fn(async t => {
+ const { errorFilter, stackDepth } = t.params;
+
+ // Push a bunch of error filters onto the stack
+ for (let i = 0; i < stackDepth; i++) {
+ t.device.pushErrorScope(kErrorScopeFilters[i % kErrorScopeFilters.length]);
+ }
+
+ // Current scope should catch the error immediately.
+ t.device.pushErrorScope(errorFilter);
+ t.generateError(errorFilter);
+ const error = await t.device.popErrorScope();
+ t.expect(t.isInstanceOfError(errorFilter, error));
+
+ // Remaining scopes shouldn't catch anything.
+ const promises = [];
+ for (let i = 0; i < stackDepth; i++) {
+ promises.push(t.device.popErrorScope());
+ }
+ const errors = await Promise.all(promises);
+ t.expect(errors.every(e => e === null));
+ });
+
+g.test('balanced_siblings')
+ .desc(
+ `
+Tests that sibling error scopes need to be balanced.
+
+- Different error types as the current scope
+- Different number of sibling errors
+ `
+ )
+ .params(u =>
+ u.combine('errorFilter', kErrorScopeFilters).combine('numErrors', [1, 10, 100, 1000])
+ )
+ .fn(async t => {
+ const { errorFilter, numErrors } = t.params;
+
+ const promises = [];
+ for (let i = 0; i < numErrors; i++) {
+ t.device.pushErrorScope(errorFilter);
+ promises.push(t.device.popErrorScope());
+ }
+
+ {
+ // Trying to pop an additional non-exisiting scope should reject.
+ const promise = t.device.popErrorScope();
+ t.shouldReject('OperationError', promise);
+ }
+
+ const errors = await Promise.all(promises);
+ t.expect(errors.every(e => e === null));
+ });
+
+g.test('balanced_nesting')
+ .desc(
+ `
+Tests that nested error scopes need to be balanced.
+
+- Different error types as the current scope
+- Different number of nested errors
+ `
+ )
+ .params(u =>
+ u.combine('errorFilter', kErrorScopeFilters).combine('numErrors', [1, 10, 100, 1000])
+ )
+ .fn(async t => {
+ const { errorFilter, numErrors } = t.params;
+
+ for (let i = 0; i < numErrors; i++) {
+ t.device.pushErrorScope(errorFilter);
+ }
+
+ const promises = [];
+ for (let i = 0; i < numErrors; i++) {
+ promises.push(t.device.popErrorScope());
+ }
+ const errors = await Promise.all(promises);
+ t.expect(errors.every(e => e === null));
+
+ {
+ // Trying to pop an additional non-exisiting scope should reject.
+ const promise = t.device.popErrorScope();
+ t.shouldReject('OperationError', promise);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/getBindGroupLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/getBindGroupLayout.spec.ts
new file mode 100644
index 0000000000..f50b8ea1ce
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/getBindGroupLayout.spec.ts
@@ -0,0 +1,201 @@
+export const description = `
+ getBindGroupLayout validation tests.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+
+import { ValidationTest } from './validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('index_range,explicit_layout')
+ .desc(
+ `
+ Test that a validation error is generated if the index exceeds the size of the bind group layouts
+ using a pipeline with an explicit layout.
+ `
+ )
+ .params(u => u.combine('index', [0, 1, 2, 3, 4, 5]))
+ .fn(async t => {
+ const { index } = t.params;
+
+ const pipelineBindGroupLayouts = t.device.createBindGroupLayout({
+ entries: [],
+ });
+
+ const kBindGroupLayoutsSizeInPipelineLayout = 1;
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [pipelineBindGroupLayouts],
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+
+ const shouldError = index >= kBindGroupLayoutsSizeInPipelineLayout;
+
+ t.expectValidationError(() => {
+ pipeline.getBindGroupLayout(index);
+ }, shouldError);
+ });
+
+g.test('index_range,auto_layout')
+ .desc(
+ `
+ Test that a validation error is generated if the index exceeds the size of the bind group layouts
+ using a pipeline with an auto layout.
+ `
+ )
+ .params(u => u.combine('index', [0, 1, 2, 3, 4, 5]))
+ .fn(async t => {
+ const { index } = t.params;
+
+ const kBindGroupLayoutsSizeInPipelineLayout = 1;
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var<uniform> binding: f32;
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ _ = binding;
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+
+ const shouldError = index >= kBindGroupLayoutsSizeInPipelineLayout;
+
+ t.expectValidationError(() => {
+ pipeline.getBindGroupLayout(index);
+ }, shouldError);
+ });
+
+g.test('unique_js_object,auto_layout')
+ .desc(
+ `
+ Test that getBindGroupLayout returns a new JavaScript object for each call.
+ `
+ )
+ .fn(async t => {
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var<uniform> binding: f32;
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ _ = binding;
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+
+ const kIndex = 0;
+ const bgl1 = (pipeline.getBindGroupLayout(kIndex) as unknown) as Record<string, number>;
+ bgl1.extra = 42;
+ const bgl2 = (pipeline.getBindGroupLayout(kIndex) as unknown) as Record<string, number>;
+
+ assert(bgl1 !== bgl2, 'objects are not the same object');
+ assert(bgl2.extra === undefined, 'objects do not retain expando properties');
+ });
+
+g.test('unique_js_object,explicit_layout')
+ .desc(
+ `
+ Test that getBindGroupLayout returns a new JavaScript object for each call.
+ `
+ )
+ .fn(async t => {
+ const pipelineBindGroupLayouts = t.device.createBindGroupLayout({
+ entries: [],
+ });
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [pipelineBindGroupLayouts],
+ });
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex
+ fn main()-> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+
+ const kIndex = 0;
+ const bgl1 = (pipeline.getBindGroupLayout(kIndex) as unknown) as Record<string, number>;
+ bgl1.extra = 42;
+ const bgl2 = (pipeline.getBindGroupLayout(kIndex) as unknown) as Record<string, number>;
+
+ assert(bgl1 !== bgl2, 'objects are not the same object');
+ assert(bgl2.extra === undefined, 'objects do not retain expando properties');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/README.txt
new file mode 100644
index 0000000000..159676f02c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/README.txt
@@ -0,0 +1,32 @@
+writeTexture + copyBufferToTexture + copyTextureToBuffer validation tests.
+
+Test coverage:
+* resource usages:
+ - texture_usage_must_be_valid: for GPUTextureUsage::COPY_SRC, GPUTextureUsage::COPY_DST flags.
+ - buffer_usage_must_be_valid: for GPUBufferUsage::COPY_SRC, GPUBufferUsage::COPY_DST flags.
+
+* textureCopyView:
+ - texture_must_be_valid: for valid, destroyed, error textures.
+ - sample_count_must_be_1: for sample count 1 and 4.
+ - mip_level_must_be_in_range: for various combinations of mipLevel and mipLevelCount.
+ - format: for all formats with full and non-full copies on width, height, and depth.
+ - texel_block_alignment_on_origin: for all formats and coordinates.
+
+* bufferCopyView:
+ - buffer_must_be_valid: for valid, destroyed, error buffers.
+ - bytes_per_row_alignment: for bytesPerRow to be 256-byte aligned or not, and bytesPerRow is required or not.
+
+* linear texture data:
+ - bound_on_rows_per_image: for various combinations of copyDepth (1, >1), copyHeight, rowsPerImage.
+ - offset_plus_required_bytes_in_copy_overflow
+ - required_bytes_in_copy: testing minimal data size and data size too small for various combinations of bytesPerRow, rowsPerImage, copyExtent and offset. for the copy method, bytesPerRow is computed as bytesInACompleteRow aligned to be a multiple of 256 + bytesPerRowPadding * 256.
+ - texel_block_alignment_on_rows_per_image: for all formats.
+ - offset_alignment: for all formats.
+ - bound_on_offset: for various combinations of offset and dataSize.
+
+* texture copy range:
+ - 1d_texture: copyExtent.height isn't 1, copyExtent.depthOrArrayLayers isn't 1.
+ - texel_block_alignment_on_size: for all formats and coordinates.
+ - texture_range_conditons: for all coordinate and various combinations of origin, copyExtent, textureSize and mipLevel.
+
+TODO: more test coverage for 1D and 3D textures.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_related.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_related.spec.ts
new file mode 100644
index 0000000000..6c186818a7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_related.spec.ts
@@ -0,0 +1,229 @@
+export const description = `Validation tests for buffer related parameters for buffer <-> texture copies`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ kSizedTextureFormats,
+ kTextureDimensions,
+ kTextureFormatInfo,
+ textureDimensionAndFormatCompatible,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { kResourceStates } from '../../../gpu_test.js';
+import { kImageCopyTypes } from '../../../util/texture/layout.js';
+
+import { ImageCopyTest, formatCopyableWithMethod } from './image_copy.js';
+
+export const g = makeTestGroup(ImageCopyTest);
+
+g.test('buffer_state')
+ .desc(
+ `
+Test that the buffer must be valid and not destroyed.
+- for all buffer <-> texture copy methods
+- for various buffer states
+`
+ )
+ .params(u =>
+ u //
+ // B2B copy validations are at api,validation,encoding,cmds,copyBufferToBuffer.spec.ts
+ .combine('method', ['CopyB2T', 'CopyT2B'] as const)
+ .combine('state', kResourceStates)
+ )
+ .fn(async t => {
+ const { method, state } = t.params;
+
+ // A valid buffer.
+ const buffer = t.createBufferWithState(state, {
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ // Invalid buffer will fail finish, and destroyed buffer will fail submit
+ const submit = state !== 'invalid';
+ const success = state === 'valid';
+
+ const texture = t.device.createTexture({
+ size: { width: 2, height: 2, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ t.testBuffer(
+ buffer,
+ texture,
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 16, method, success, submit }
+ );
+ });
+
+g.test('buffer,device_mismatch')
+ .desc('Tests the image copies cannot be called with a buffer created from another device')
+ .paramsSubcasesOnly(u =>
+ u.combine('method', ['CopyB2T', 'CopyT2B'] as const).combine('mismatched', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { method, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const buffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(buffer);
+
+ const texture = t.device.createTexture({
+ size: { width: 2, height: 2, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const success = !mismatched;
+
+ // Expect success in both finish and submit, or validation error in finish
+ t.testBuffer(
+ buffer,
+ texture,
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 16, method, success, submit: success }
+ );
+ });
+
+g.test('usage')
+ .desc(
+ `
+Test the buffer must have the appropriate COPY_SRC/COPY_DST usage.
+TODO update such that it tests
+- for all buffer source usages
+- for all buffer destination usages
+`
+ )
+ .params(u =>
+ u
+ // B2B copy validations are at api,validation,encoding,cmds,copyBufferToBuffer.spec.ts
+ .combine('method', ['CopyB2T', 'CopyT2B'] as const)
+ .beginSubcases()
+ .combine('usage', [
+ GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.UNIFORM,
+ GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.UNIFORM,
+ GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.COPY_DST,
+ ])
+ )
+ .fn(async t => {
+ const { method, usage } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 16,
+ usage,
+ });
+
+ const success =
+ method === 'CopyB2T'
+ ? (usage & GPUBufferUsage.COPY_SRC) !== 0
+ : (usage & GPUBufferUsage.COPY_DST) !== 0;
+
+ const texture = t.device.createTexture({
+ size: { width: 2, height: 2, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ // Expect success in both finish and submit, or validation error in finish
+ t.testBuffer(
+ buffer,
+ texture,
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 16, method, success, submit: success }
+ );
+ });
+
+g.test('bytes_per_row_alignment')
+ .desc(
+ `
+Test that bytesPerRow must be a multiple of 256 for CopyB2T and CopyT2B if it is required.
+- for all copy methods between linear data and textures
+- for all texture dimensions
+- for all sized formats.
+- for various bytesPerRow aligned to 256 or not
+- for various number of blocks rows copied
+`
+ )
+ .params(u =>
+ u //
+ .combine('method', kImageCopyTypes)
+ .combine('format', kSizedTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('bytesPerRow', [undefined, 0, 1, 255, 256, 257, 512])
+ .combine('copyHeightInBlocks', [0, 1, 2, 3])
+ .expand('_textureHeightInBlocks', p => [
+ p.copyHeightInBlocks === 0 ? 1 : p.copyHeightInBlocks,
+ ])
+ .unless(p => p.dimension === '1d' && p.copyHeightInBlocks > 1)
+ // Depth/stencil format copies must copy the whole subresource.
+ .unless(p => {
+ const info = kTextureFormatInfo[p.format];
+ return (info.depth || info.stencil) && p.copyHeightInBlocks !== p._textureHeightInBlocks;
+ })
+ // bytesPerRow must be specified and it must be equal or greater than the bytes size of each row if we are copying multiple rows.
+ // Note that we are copying one single block on each row in this test.
+ .filter(
+ ({ format, bytesPerRow, copyHeightInBlocks }) =>
+ (bytesPerRow === undefined && copyHeightInBlocks <= 1) ||
+ (bytesPerRow !== undefined && bytesPerRow >= kTextureFormatInfo[format].bytesPerBlock)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ method,
+ dimension,
+ format,
+ bytesPerRow,
+ copyHeightInBlocks,
+ _textureHeightInBlocks,
+ } = t.params;
+
+ const info = kTextureFormatInfo[format];
+
+ const buffer = t.device.createBuffer({
+ size: 512 * 8 * 16,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ let success = false;
+ // writeTexture doesn't require bytesPerRow to be 256-byte aligned.
+ if (method === 'WriteTexture') success = true;
+ // If the copy height <= 1, bytesPerRow is not required.
+ if (copyHeightInBlocks <= 1 && bytesPerRow === undefined) success = true;
+ // If bytesPerRow > 0 and it is a multiple of 256, it will succeed if other parameters are valid.
+ if (bytesPerRow !== undefined && bytesPerRow > 0 && bytesPerRow % 256 === 0) success = true;
+
+ const size = [info.blockWidth, _textureHeightInBlocks * info.blockHeight, 1];
+ const texture = t.device.createTexture({
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const copySize = [info.blockWidth, copyHeightInBlocks * info.blockHeight, 1];
+
+ // Expect success in both finish and submit, or validation error in finish
+ t.testBuffer(buffer, texture, { bytesPerRow }, copySize, {
+ dataSize: 512 * 8 * 16,
+ method,
+ success,
+ submit: success,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts
new file mode 100644
index 0000000000..12764d4512
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts
@@ -0,0 +1,454 @@
+export const description = `
+copyTextureToBuffer and copyBufferToTexture validation tests not covered by
+the general image_copy tests, or by destroyed,*.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../../common/util/util.js';
+import {
+ kDepthStencilFormats,
+ kBufferUsages,
+ kTextureUsages,
+ depthStencilBufferTextureCopySupported,
+ depthStencilFormatAspectSize,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { align } from '../../../util/math.js';
+import { kBufferCopyAlignment, kBytesPerRowAlignment } from '../../../util/texture/layout.js';
+import { ValidationTest } from '../validation_test.js';
+
+class ImageCopyTest extends ValidationTest {
+ testCopyBufferToTexture(
+ source: GPUImageCopyBuffer,
+ destination: GPUImageCopyTexture,
+ copySize: GPUExtent3DStrict,
+ isSuccess: boolean
+ ): void {
+ const { encoder, validateFinishAndSubmit } = this.createEncoder('non-pass');
+ encoder.copyBufferToTexture(source, destination, copySize);
+ validateFinishAndSubmit(isSuccess, true);
+ }
+
+ testCopyTextureToBuffer(
+ source: GPUImageCopyTexture,
+ destination: GPUImageCopyBuffer,
+ copySize: GPUExtent3DStrict,
+ isSuccess: boolean
+ ): void {
+ const { encoder, validateFinishAndSubmit } = this.createEncoder('non-pass');
+ encoder.copyTextureToBuffer(source, destination, copySize);
+ validateFinishAndSubmit(isSuccess, true);
+ }
+
+ testWriteTexture(
+ destination: GPUImageCopyTexture,
+ uploadData: Uint8Array,
+ dataLayout: GPUImageDataLayout,
+ copySize: GPUExtent3DStrict,
+ isSuccess: boolean
+ ): void {
+ this.expectGPUError(
+ 'validation',
+ () => this.queue.writeTexture(destination, uploadData, dataLayout, copySize),
+ !isSuccess
+ );
+ }
+}
+
+export const g = makeTestGroup(ImageCopyTest);
+
+g.test('depth_stencil_format,copy_usage_and_aspect')
+ .desc(
+ `
+ Validate the combination of usage and aspect of each depth stencil format in copyBufferToTexture,
+ copyTextureToBuffer and writeTexture. See https://gpuweb.github.io/gpuweb/#depth-formats for more
+ details.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('aspect', ['all', 'depth-only', 'stencil-only'] as const)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const { format, aspect } = t.params;
+
+ const textureSize = { width: 1, height: 1, depthOrArrayLayers: 1 };
+ const texture = t.device.createTexture({
+ size: textureSize,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const uploadBufferSize = 32;
+ const buffer = t.device.createBuffer({
+ size: uploadBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ {
+ const success = depthStencilBufferTextureCopySupported('CopyB2T', format, aspect);
+ t.testCopyBufferToTexture({ buffer }, { texture, aspect }, textureSize, success);
+ }
+
+ {
+ const success = depthStencilBufferTextureCopySupported('CopyT2B', format, aspect);
+ t.testCopyTextureToBuffer({ texture, aspect }, { buffer }, textureSize, success);
+ }
+
+ {
+ const success = depthStencilBufferTextureCopySupported('WriteTexture', format, aspect);
+ const uploadData = new Uint8Array(uploadBufferSize);
+ t.testWriteTexture({ texture, aspect }, uploadData, {}, textureSize, success);
+ }
+ });
+
+g.test('depth_stencil_format,copy_buffer_size')
+ .desc(
+ `
+ Validate the minimum buffer size for each depth stencil format in copyBufferToTexture,
+ copyTextureToBuffer and writeTexture.
+
+ Given a depth stencil format, a copy aspect ('depth-only' or 'stencil-only'), the copy method
+ (buffer-to-texture or texture-to-buffer) and the copy size, validate
+ - if the copy can be successfully executed with the minimum required buffer size.
+ - if the copy fails with a validation error when the buffer size is less than the minimum
+ required buffer size.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .combine('aspect', ['depth-only', 'stencil-only'] as const)
+ .combine('copyType', ['CopyB2T', 'CopyT2B', 'WriteTexture'] as const)
+ .filter(param =>
+ depthStencilBufferTextureCopySupported(param.copyType, param.format, param.aspect)
+ )
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 8, height: 1, depthOrArrayLayers: 1 },
+ { width: 4, height: 4, depthOrArrayLayers: 1 },
+ { width: 4, height: 4, depthOrArrayLayers: 3 },
+ ])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const { format, aspect, copyType, copySize } = t.params;
+
+ const texture = t.device.createTexture({
+ size: copySize,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const texelAspectSize = depthStencilFormatAspectSize(format, aspect);
+ assert(texelAspectSize > 0);
+
+ const bytesPerRowAlignment = copyType === 'WriteTexture' ? 1 : kBytesPerRowAlignment;
+ const bytesPerRow = align(texelAspectSize * copySize.width, bytesPerRowAlignment);
+ const rowsPerImage = copySize.height;
+ const minimumBufferSize =
+ bytesPerRow * (rowsPerImage * copySize.depthOrArrayLayers - 1) +
+ align(texelAspectSize * copySize.width, kBufferCopyAlignment);
+ assert(minimumBufferSize > kBufferCopyAlignment);
+
+ const bigEnoughBuffer = t.device.createBuffer({
+ size: minimumBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ const smallerBuffer = t.device.createBuffer({
+ size: minimumBufferSize - kBufferCopyAlignment,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ if (copyType === 'CopyB2T') {
+ t.testCopyBufferToTexture(
+ { buffer: bigEnoughBuffer, bytesPerRow, rowsPerImage },
+ { texture, aspect },
+ copySize,
+ true
+ );
+ t.testCopyBufferToTexture(
+ { buffer: smallerBuffer, bytesPerRow, rowsPerImage },
+ { texture, aspect },
+ copySize,
+ false
+ );
+ } else if (copyType === 'CopyT2B') {
+ t.testCopyTextureToBuffer(
+ { texture, aspect },
+ { buffer: bigEnoughBuffer, bytesPerRow, rowsPerImage },
+ copySize,
+ true
+ );
+ t.testCopyTextureToBuffer(
+ { texture, aspect },
+ { buffer: smallerBuffer, bytesPerRow, rowsPerImage },
+ copySize,
+ false
+ );
+ } else if (copyType === 'WriteTexture') {
+ const enoughUploadData = new Uint8Array(minimumBufferSize);
+ const smallerUploadData = new Uint8Array(minimumBufferSize - kBufferCopyAlignment);
+ t.testWriteTexture(
+ { texture, aspect },
+ enoughUploadData,
+ {
+ bytesPerRow,
+ rowsPerImage,
+ },
+ copySize,
+ true
+ );
+
+ t.testWriteTexture(
+ { texture, aspect },
+ smallerUploadData,
+ {
+ bytesPerRow,
+ rowsPerImage,
+ },
+ copySize,
+ false
+ );
+ } else {
+ unreachable();
+ }
+ });
+
+g.test('depth_stencil_format,copy_buffer_offset')
+ .desc(
+ `
+ Validate for every depth stencil formats the buffer offset must be a multiple of 4 in
+ copyBufferToTexture() and copyTextureToBuffer(), but the offset in writeTexture() doesn't always
+ need to be a multiple of 4.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kDepthStencilFormats)
+ .combine('aspect', ['depth-only', 'stencil-only'] as const)
+ .combine('copyType', ['CopyB2T', 'CopyT2B', 'WriteTexture'] as const)
+ .filter(param =>
+ depthStencilBufferTextureCopySupported(param.copyType, param.format, param.aspect)
+ )
+ .beginSubcases()
+ .combine('offset', [1, 2, 4, 6, 8])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceForTextureFormatOrSkipTestCase(format);
+ })
+ .fn(async t => {
+ const { format, aspect, copyType, offset } = t.params;
+
+ const textureSize = { width: 4, height: 4, depthOrArrayLayers: 1 };
+
+ const texture = t.device.createTexture({
+ size: textureSize,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const texelAspectSize = depthStencilFormatAspectSize(format, aspect);
+ assert(texelAspectSize > 0);
+
+ const bytesPerRowAlignment = copyType === 'WriteTexture' ? 1 : kBytesPerRowAlignment;
+ const bytesPerRow = align(texelAspectSize * textureSize.width, bytesPerRowAlignment);
+ const rowsPerImage = textureSize.height;
+ const minimumBufferSize =
+ bytesPerRow * (rowsPerImage * textureSize.depthOrArrayLayers - 1) +
+ align(texelAspectSize * textureSize.width, kBufferCopyAlignment);
+ assert(minimumBufferSize > kBufferCopyAlignment);
+
+ const buffer = t.device.createBuffer({
+ size: align(minimumBufferSize + offset, kBufferCopyAlignment),
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const isSuccess = copyType === 'WriteTexture' ? true : offset % 4 === 0;
+
+ if (copyType === 'CopyB2T') {
+ t.testCopyBufferToTexture(
+ { buffer, offset, bytesPerRow, rowsPerImage },
+ { texture, aspect },
+ textureSize,
+ isSuccess
+ );
+ } else if (copyType === 'CopyT2B') {
+ t.testCopyTextureToBuffer(
+ { texture, aspect },
+ { buffer, offset, bytesPerRow, rowsPerImage },
+ textureSize,
+ isSuccess
+ );
+ } else if (copyType === 'WriteTexture') {
+ const uploadData = new Uint8Array(minimumBufferSize + offset);
+ t.testWriteTexture(
+ { texture, aspect },
+ uploadData,
+ {
+ offset,
+ bytesPerRow,
+ rowsPerImage,
+ },
+ textureSize,
+ isSuccess
+ );
+ } else {
+ unreachable();
+ }
+ });
+
+g.test('sample_count')
+ .desc(
+ `
+ Test that the texture sample count. Check that a validation error is generated if sample count is
+ not 1.
+ `
+ )
+ .params(u =>
+ u //
+ // writeTexture is handled by writeTexture.spec.ts.
+ .combine('copyType', ['CopyB2T', 'CopyT2B'] as const)
+ .beginSubcases()
+ .combine('sampleCount', [1, 4])
+ )
+ .fn(async t => {
+ const { sampleCount, copyType } = t.params;
+
+ let usage = GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST;
+ // WebGPU SPEC requires multisampled textures must have RENDER_ATTACHMENT usage.
+ if (sampleCount > 1) {
+ usage |= GPUTextureUsage.RENDER_ATTACHMENT;
+ }
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16 },
+ sampleCount,
+ format: 'bgra8unorm',
+ usage,
+ });
+
+ const uploadBufferSize = 32;
+ const buffer = t.device.createBuffer({
+ size: uploadBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+
+ const textureSize = { width: 1, height: 1, depthOrArrayLayers: 1 };
+
+ const isSuccess = sampleCount === 1;
+
+ if (copyType === 'CopyB2T') {
+ t.testCopyBufferToTexture({ buffer }, { texture }, textureSize, isSuccess);
+ } else if (copyType === 'CopyT2B') {
+ t.testCopyTextureToBuffer({ texture }, { buffer }, textureSize, isSuccess);
+ }
+ });
+
+const kRequiredTextureUsage = {
+ CopyT2B: GPUConst.TextureUsage.COPY_SRC,
+ CopyB2T: GPUConst.TextureUsage.COPY_DST,
+};
+const kRequiredBufferUsage = {
+ CopyB2T: GPUConst.BufferUsage.COPY_SRC,
+ CopyT2B: GPUConst.BufferUsage.COPY_DST,
+};
+
+g.test('texture_buffer_usages')
+ .desc(
+ `
+ Tests calling copyTextureToBuffer or copyBufferToTexture with the texture and the buffer missed
+ COPY_SRC, COPY_DST usage respectively.
+ - texture and buffer {with, without} COPY_SRC and COPY_DST usage.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('copyType', ['CopyB2T', 'CopyT2B'] as const)
+ .beginSubcases()
+ .combine('textureUsage', kTextureUsages)
+ .expand('_textureUsageValid', p => [p.textureUsage === kRequiredTextureUsage[p.copyType]])
+ .combine('bufferUsage', kBufferUsages)
+ .expand('_bufferUsageValid', p => [p.bufferUsage === kRequiredBufferUsage[p.copyType]])
+ .filter(p => p._textureUsageValid || p._bufferUsageValid)
+ )
+ .fn(async t => {
+ const { copyType, textureUsage, _textureUsageValid, bufferUsage, _bufferUsageValid } = t.params;
+
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16 },
+ format: 'rgba8unorm',
+ usage: textureUsage,
+ });
+
+ const uploadBufferSize = 32;
+ const buffer = t.device.createBuffer({
+ size: uploadBufferSize,
+ usage: bufferUsage,
+ });
+
+ const textureSize = { width: 1, height: 1, depthOrArrayLayers: 1 };
+
+ const isSuccess = _textureUsageValid && _bufferUsageValid;
+ if (copyType === 'CopyB2T') {
+ t.testCopyBufferToTexture({ buffer }, { texture }, textureSize, isSuccess);
+ } else if (copyType === 'CopyT2B') {
+ t.testCopyTextureToBuffer({ texture }, { buffer }, textureSize, isSuccess);
+ }
+ });
+
+g.test('device_mismatch')
+ .desc(
+ `
+ Tests copyBufferToTexture and copyTextureToBuffer cannot be called with a buffer or a texture
+ created from another device.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('copyType', ['CopyB2T', 'CopyT2B'] as const)
+ .beginSubcases()
+ .combineWithParams([
+ { bufMismatched: false, texMismatched: false }, // control case
+ { bufMismatched: true, texMismatched: false },
+ { bufMismatched: false, texMismatched: true },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { copyType, bufMismatched, texMismatched } = t.params;
+
+ const uploadBufferSize = 32;
+ const buffer = (bufMismatched ? t.mismatchedDevice : t.device).createBuffer({
+ size: uploadBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(buffer);
+
+ const textureSize = { width: 1, height: 1, depthOrArrayLayers: 1 };
+ const texture = (texMismatched ? t.mismatchedDevice : t.device).createTexture({
+ size: textureSize,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+ t.trackForCleanup(texture);
+
+ const isValid = !bufMismatched && !texMismatched;
+
+ if (copyType === 'CopyB2T') {
+ t.testCopyBufferToTexture({ buffer }, { texture }, textureSize, isValid);
+ } else if (copyType === 'CopyT2B') {
+ t.testCopyTextureToBuffer({ texture }, { buffer }, textureSize, isValid);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts
new file mode 100644
index 0000000000..bf36500057
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/image_copy.ts
@@ -0,0 +1,267 @@
+import {
+ kTextureFormatInfo,
+ SizedTextureFormat,
+ DepthStencilFormat,
+ depthStencilFormatCopyableAspects,
+} from '../../../capability_info.js';
+import { align } from '../../../util/math.js';
+import { ImageCopyType } from '../../../util/texture/layout.js';
+import { ValidationTest } from '../validation_test.js';
+
+export class ImageCopyTest extends ValidationTest {
+ testRun(
+ textureCopyView: GPUImageCopyTexture,
+ textureDataLayout: GPUImageDataLayout,
+ size: GPUExtent3D,
+ {
+ method,
+ dataSize,
+ success,
+ submit = false,
+ }: {
+ method: ImageCopyType;
+ dataSize: number;
+ success: boolean;
+ /** If submit is true, the validation error is expected to come from the submit and encoding
+ * should succeed. */
+ submit?: boolean;
+ }
+ ): void {
+ switch (method) {
+ case 'WriteTexture': {
+ const data = new Uint8Array(dataSize);
+
+ this.expectValidationError(() => {
+ this.device.queue.writeTexture(textureCopyView, data, textureDataLayout, size);
+ }, !success);
+
+ break;
+ }
+ case 'CopyB2T': {
+ const buffer = this.device.createBuffer({
+ size: dataSize,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ this.trackForCleanup(buffer);
+
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyBufferToTexture({ buffer, ...textureDataLayout }, textureCopyView, size);
+
+ if (submit) {
+ const cmd = encoder.finish();
+ this.expectValidationError(() => {
+ this.device.queue.submit([cmd]);
+ }, !success);
+ } else {
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+
+ break;
+ }
+ case 'CopyT2B': {
+ const buffer = this.device.createBuffer({
+ size: dataSize,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(buffer);
+
+ const encoder = this.device.createCommandEncoder();
+ encoder.copyTextureToBuffer(textureCopyView, { buffer, ...textureDataLayout }, size);
+
+ if (submit) {
+ const cmd = encoder.finish();
+ this.expectValidationError(() => {
+ this.device.queue.submit([cmd]);
+ }, !success);
+ } else {
+ this.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Creates a texture when all that is needed is an aligned texture given the format and desired
+ * dimensions/origin. The resultant texture guarantees that a copy with the same size and origin
+ * should be possible.
+ */
+ createAlignedTexture(
+ format: SizedTextureFormat,
+ size: Required<GPUExtent3DDict> = {
+ width: 1,
+ height: 1,
+ depthOrArrayLayers: 1,
+ },
+ origin: Required<GPUOrigin3DDict> = { x: 0, y: 0, z: 0 },
+ dimension: Required<GPUTextureDimension> = '2d'
+ ): GPUTexture {
+ const info = kTextureFormatInfo[format];
+ const alignedSize = {
+ width: align(Math.max(1, size.width + origin.x), info.blockWidth),
+ height: align(Math.max(1, size.height + origin.y), info.blockHeight),
+ depthOrArrayLayers: Math.max(1, size.depthOrArrayLayers + origin.z),
+ };
+ return this.device.createTexture({
+ size: alignedSize,
+ dimension,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+ }
+
+ testBuffer(
+ buffer: GPUBuffer,
+ texture: GPUTexture,
+ textureDataLayout: GPUImageDataLayout,
+ size: GPUExtent3D,
+ {
+ method,
+ dataSize,
+ success,
+ submit = true,
+ }: {
+ method: ImageCopyType;
+ dataSize: number;
+ success: boolean;
+ /** If submit is true, the validation error is expected to come from the submit and encoding
+ * should succeed. */
+ submit?: boolean;
+ }
+ ): void {
+ switch (method) {
+ case 'WriteTexture': {
+ const data = new Uint8Array(dataSize);
+
+ this.expectValidationError(() => {
+ this.device.queue.writeTexture({ texture }, data, textureDataLayout, size);
+ }, !success);
+
+ break;
+ }
+ case 'CopyB2T': {
+ const { encoder, validateFinish, validateFinishAndSubmit } = this.createEncoder('non-pass');
+ encoder.copyBufferToTexture({ buffer, ...textureDataLayout }, { texture }, size);
+
+ if (submit) {
+ // validation error is expected to come from the submit and encoding should succeed
+ validateFinishAndSubmit(true, success);
+ } else {
+ // validation error is expected to come from the encoding
+ validateFinish(success);
+ }
+
+ break;
+ }
+ case 'CopyT2B': {
+ const { encoder, validateFinish, validateFinishAndSubmit } = this.createEncoder('non-pass');
+ encoder.copyTextureToBuffer({ texture }, { buffer, ...textureDataLayout }, size);
+
+ if (submit) {
+ // validation error is expected to come from the submit and encoding should succeed
+ validateFinishAndSubmit(true, success);
+ } else {
+ // validation error is expected to come from the encoding
+ validateFinish(success);
+ }
+
+ break;
+ }
+ }
+ }
+}
+
+// For testing divisibility by a number we test all the values returned by this function:
+function valuesToTestDivisibilityBy(number: number): Iterable<number> {
+ const values = [];
+ for (let i = 0; i <= 2 * number; ++i) {
+ values.push(i);
+ }
+ values.push(3 * number);
+ return values;
+}
+
+interface WithFormat {
+ format: SizedTextureFormat;
+}
+
+interface WithFormatAndCoordinate extends WithFormat {
+ coordinateToTest: keyof GPUOrigin3DDict | keyof GPUExtent3DDict;
+}
+
+interface WithFormatAndMethod extends WithFormat {
+ method: ImageCopyType;
+}
+
+// This is a helper function used for expanding test parameters for offset alignment, by spec
+export function texelBlockAlignmentTestExpanderForOffset({ format }: WithFormat) {
+ const info = kTextureFormatInfo[format];
+ if (info.depth || info.stencil) {
+ return valuesToTestDivisibilityBy(4);
+ }
+
+ return valuesToTestDivisibilityBy(kTextureFormatInfo[format].bytesPerBlock);
+}
+
+// This is a helper function used for expanding test parameters for texel block alignment tests on rowsPerImage
+export function texelBlockAlignmentTestExpanderForRowsPerImage({ format }: WithFormat) {
+ return valuesToTestDivisibilityBy(kTextureFormatInfo[format].blockHeight);
+}
+
+// This is a helper function used for expanding test parameters for texel block alignment tests on origin and size
+export function texelBlockAlignmentTestExpanderForValueToCoordinate({
+ format,
+ coordinateToTest,
+}: WithFormatAndCoordinate) {
+ switch (coordinateToTest) {
+ case 'x':
+ case 'width':
+ return valuesToTestDivisibilityBy(kTextureFormatInfo[format].blockWidth);
+
+ case 'y':
+ case 'height':
+ return valuesToTestDivisibilityBy(kTextureFormatInfo[format].blockHeight);
+
+ case 'z':
+ case 'depthOrArrayLayers':
+ return valuesToTestDivisibilityBy(1);
+ }
+}
+
+// This is a helper function used for filtering test parameters
+export function formatCopyableWithMethod({ format, method }: WithFormatAndMethod): boolean {
+ const info = kTextureFormatInfo[format];
+ if (info.depth || info.stencil) {
+ const supportedAspects: readonly GPUTextureAspect[] = depthStencilFormatCopyableAspects(
+ method,
+ format as DepthStencilFormat
+ );
+ return supportedAspects.length > 0;
+ }
+ if (method === 'CopyT2B') {
+ return info.copySrc;
+ } else {
+ return info.copyDst;
+ }
+}
+
+// This is a helper function used for filtering test parameters
+export function getACopyableAspectWithMethod({
+ format,
+ method,
+}: WithFormatAndMethod): GPUTextureAspect {
+ const info = kTextureFormatInfo[format];
+ if (info.depth || info.stencil) {
+ const supportedAspects: readonly GPUTextureAspect[] = depthStencilFormatCopyableAspects(
+ method,
+ format as DepthStencilFormat
+ );
+ return supportedAspects[0];
+ }
+ return 'all' as GPUTextureAspect;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/layout_related.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/layout_related.spec.ts
new file mode 100644
index 0000000000..9c632c729c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/layout_related.spec.ts
@@ -0,0 +1,479 @@
+export const description = `Validation tests for the linear data layout of linear data <-> texture copies
+
+TODO check if the tests need to be updated to support aspects of depth-stencil textures`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ kSizedTextureFormats,
+ textureDimensionAndFormatCompatible,
+ kTextureDimensions,
+} from '../../../capability_info.js';
+import { align } from '../../../util/math.js';
+import {
+ bytesInACompleteRow,
+ dataBytesForCopyOrOverestimate,
+ dataBytesForCopyOrFail,
+ kImageCopyTypes,
+} from '../../../util/texture/layout.js';
+
+import {
+ ImageCopyTest,
+ texelBlockAlignmentTestExpanderForOffset,
+ texelBlockAlignmentTestExpanderForRowsPerImage,
+ formatCopyableWithMethod,
+} from './image_copy.js';
+
+export const g = makeTestGroup(ImageCopyTest);
+
+g.test('bound_on_rows_per_image')
+ .desc(
+ `
+Test that rowsPerImage must be at least the copy height (if defined).
+- for various copy methods
+- for all texture dimensions
+- for various values of rowsPerImage including undefined
+- for various copy heights
+- for various copy depths
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combineWithParams([
+ { dimension: '1d', size: [4, 1, 1] },
+ { dimension: '2d', size: [4, 4, 1] },
+ { dimension: '2d', size: [4, 4, 3] },
+ { dimension: '3d', size: [4, 4, 3] },
+ ] as const)
+ .beginSubcases()
+ .combine('rowsPerImage', [undefined, 0, 1, 2, 1024])
+ .combine('copyHeightInBlocks', [0, 1, 2])
+ .combine('copyDepth', [1, 3])
+ .unless(p => p.dimension === '1d' && p.copyHeightInBlocks !== 1)
+ .unless(p => p.copyDepth > p.size[2])
+ )
+ .fn(async t => {
+ const { rowsPerImage, copyHeightInBlocks, copyDepth, dimension, size, method } = t.params;
+
+ const format = 'rgba8unorm';
+ const copyHeight = copyHeightInBlocks * kTextureFormatInfo[format].blockHeight;
+
+ const texture = t.device.createTexture({
+ size,
+ dimension,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const layout = { bytesPerRow: 1024, rowsPerImage };
+ const copySize = { width: 0, height: copyHeight, depthOrArrayLayers: copyDepth };
+ const { minDataSizeOrOverestimate, copyValid } = dataBytesForCopyOrOverestimate({
+ layout,
+ format,
+ copySize,
+ method,
+ });
+
+ t.testRun({ texture }, layout, copySize, {
+ dataSize: minDataSizeOrOverestimate,
+ method,
+ success: copyValid,
+ });
+ });
+
+g.test('copy_end_overflows_u64')
+ .desc(
+ `
+Test an error is produced when offset+requiredBytesInCopy overflows GPUSize64.
+- for various copy methods
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .beginSubcases()
+ .combineWithParams([
+ { bytesPerRow: 2 ** 31, rowsPerImage: 2 ** 31, depthOrArrayLayers: 1, _success: true }, // success case
+ { bytesPerRow: 2 ** 31, rowsPerImage: 2 ** 31, depthOrArrayLayers: 16, _success: false }, // bytesPerRow * rowsPerImage * (depthOrArrayLayers - 1) overflows.
+ ])
+ )
+ .fn(async t => {
+ const { method, bytesPerRow, rowsPerImage, depthOrArrayLayers, _success } = t.params;
+
+ const texture = t.device.createTexture({
+ size: [1, 1, depthOrArrayLayers],
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ t.testRun(
+ { texture },
+ { bytesPerRow, rowsPerImage },
+ { width: 1, height: 1, depthOrArrayLayers },
+ {
+ dataSize: 10000,
+ method,
+ success: _success,
+ }
+ );
+ });
+
+g.test('required_bytes_in_copy')
+ .desc(
+ `
+Test the computation of requiredBytesInCopy by computing the minimum data size for the copy and checking success/error at the boundary.
+- for various copy methods
+- for all formats
+- for all dimensions
+- for various extra bytesPerRow/rowsPerImage
+- for various copy sizes
+- for various offsets in the linear data
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combine('format', kSizedTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combineWithParams([
+ { bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 0 }, // no padding
+ { bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 6 }, // rowsPerImage padding
+ { bytesPerRowPadding: 6, rowsPerImagePaddingInBlocks: 0 }, // bytesPerRow padding
+ { bytesPerRowPadding: 15, rowsPerImagePaddingInBlocks: 17 }, // both paddings
+ ])
+ .combineWithParams([
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 }, // standard copy
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 3, _offsetMultiplier: 11 }, // standard copy, offset > 0
+ { copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2, _offsetMultiplier: 0 }, // copyWidth is 256-aligned
+ { copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 }, // empty copy because of width
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5, _offsetMultiplier: 0 }, // empty copy because of height
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0, _offsetMultiplier: 13 }, // empty copy because of depth, offset > 0
+ { copyWidthInBlocks: 1, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 }, // copyWidth = 1
+ { copyWidthInBlocks: 3, copyHeightInBlocks: 1, copyDepth: 5, _offsetMultiplier: 15 }, // copyHeight = 1, offset > 0
+ { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1, _offsetMultiplier: 0 }, // copyDepth = 1
+ { copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1, _offsetMultiplier: 0 }, // copyHeight = 1 and copyDepth = 1
+ ])
+ // The test texture size will be rounded up from the copy size to the next valid texture size.
+ // If the format is a depth/stencil format, its copy size must equal to subresource's size.
+ // So filter out depth/stencil cases where the rounded-up texture size would be different from the copy size.
+ .filter(({ format, copyWidthInBlocks, copyHeightInBlocks, copyDepth }) => {
+ const info = kTextureFormatInfo[format];
+ return (
+ (!info.depth && !info.stencil) ||
+ (copyWidthInBlocks > 0 && copyHeightInBlocks > 0 && copyDepth > 0)
+ );
+ })
+ .unless(p => p.dimension === '1d' && (p.copyHeightInBlocks > 1 || p.copyDepth > 1))
+ .expand('offset', p => {
+ const info = kTextureFormatInfo[p.format];
+ if (info.depth || info.stencil) {
+ return [p._offsetMultiplier * 4];
+ }
+ return [p._offsetMultiplier * info.bytesPerBlock];
+ })
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ offset,
+ bytesPerRowPadding,
+ rowsPerImagePaddingInBlocks,
+ copyWidthInBlocks,
+ copyHeightInBlocks,
+ copyDepth,
+ format,
+ dimension,
+ method,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ // In the CopyB2T and CopyT2B cases we need to have bytesPerRow 256-aligned,
+ // to make this happen we align the bytesInACompleteRow value and multiply
+ // bytesPerRowPadding by 256.
+ const bytesPerRowAlignment = method === 'WriteTexture' ? 1 : 256;
+ const copyWidth = copyWidthInBlocks * info.blockWidth;
+ const copyHeight = copyHeightInBlocks * info.blockHeight;
+ const rowsPerImage = copyHeight + rowsPerImagePaddingInBlocks * info.blockHeight;
+ const bytesPerRow =
+ align(bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) +
+ bytesPerRowPadding * bytesPerRowAlignment;
+ const copySize = { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth };
+
+ const layout = { offset, bytesPerRow, rowsPerImage };
+ const minDataSize = dataBytesForCopyOrFail({ layout, format, copySize, method });
+
+ const texture = t.createAlignedTexture(format, copySize, undefined, dimension);
+
+ t.testRun({ texture }, layout, copySize, {
+ dataSize: minDataSize,
+ method,
+ success: true,
+ });
+
+ if (minDataSize > 0) {
+ t.testRun({ texture }, layout, copySize, {
+ dataSize: minDataSize - 1,
+ method,
+ success: false,
+ });
+ }
+ });
+
+g.test('rows_per_image_alignment')
+ .desc(
+ `
+Test that rowsPerImage has no alignment constraints.
+- for various copy methods
+- for all sized format
+- for all dimensions
+- for various rowsPerImage
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combine('format', kSizedTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .expand('rowsPerImage', texelBlockAlignmentTestExpanderForRowsPerImage)
+ // Copy height is info.blockHeight, so rowsPerImage must be equal or greater than it.
+ .filter(({ rowsPerImage, format }) => rowsPerImage >= kTextureFormatInfo[format].blockHeight)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { rowsPerImage, format, method } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const size = { width: info.blockWidth, height: info.blockHeight, depthOrArrayLayers: 1 };
+ const texture = t.device.createTexture({
+ size,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ t.testRun({ texture }, { bytesPerRow: 256, rowsPerImage }, size, {
+ dataSize: info.bytesPerBlock,
+ method,
+ success: true,
+ });
+ });
+
+g.test('offset_alignment')
+ .desc(
+ `
+Test the alignment requirement on the linear data offset (block size, or 4 for depth-stencil).
+- for various copy methods
+- for all sized formats
+- for all dimensions
+- for various linear data offsets
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combine('format', kSizedTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .expand('offset', texelBlockAlignmentTestExpanderForOffset)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { format, offset, method } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const size = { width: info.blockWidth, height: info.blockHeight, depthOrArrayLayers: 1 };
+ const texture = t.device.createTexture({
+ size,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ let success = false;
+ if (method === 'WriteTexture') success = true;
+ if (info.depth || info.stencil) {
+ if (offset % 4 === 0) success = true;
+ } else {
+ if (offset % info.bytesPerBlock === 0) success = true;
+ }
+
+ t.testRun({ texture }, { offset, bytesPerRow: 256 }, size, {
+ dataSize: offset + info.bytesPerBlock,
+ method,
+ success,
+ });
+ });
+
+g.test('bound_on_bytes_per_row')
+ .desc(
+ `
+Test that bytesPerRow, if specified must be big enough for a full copy row.
+- for various copy methods
+- for all sized formats
+- for all dimension
+- for various copy heights
+- for various copy depths
+- for various combinations of bytesPerRow and copy width.
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combine('format', kSizedTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('copyHeightInBlocks', [1, 2])
+ .combine('copyDepth', [1, 2])
+ .unless(p => p.dimension === '1d' && (p.copyHeightInBlocks > 1 || p.copyDepth > 1))
+ .expandWithParams(p => {
+ const info = kTextureFormatInfo[p.format];
+ // We currently have a built-in assumption that for all formats, 128 % bytesPerBlock === 0.
+ // This assumption ensures that all division below results in integers.
+ assert(128 % info.bytesPerBlock === 0);
+ return [
+ // Copying exact fit with aligned bytesPerRow should work.
+ {
+ bytesPerRow: 256,
+ widthInBlocks: 256 / info.bytesPerBlock,
+ copyWidthInBlocks: 256 / info.bytesPerBlock,
+ _success: true,
+ },
+ // Copying into smaller texture when padding in bytesPerRow is enough should work unless
+ // it is a depth/stencil typed format.
+ {
+ bytesPerRow: 256,
+ widthInBlocks: 256 / info.bytesPerBlock,
+ copyWidthInBlocks: 256 / info.bytesPerBlock - 1,
+ _success: !(info.stencil || info.depth),
+ },
+ // Unaligned bytesPerRow should not work unless the method is 'WriteTexture'.
+ {
+ bytesPerRow: 128,
+ widthInBlocks: 128 / info.bytesPerBlock,
+ copyWidthInBlocks: 128 / info.bytesPerBlock,
+ _success: p.method === 'WriteTexture',
+ },
+ {
+ bytesPerRow: 384,
+ widthInBlocks: 384 / info.bytesPerBlock,
+ copyWidthInBlocks: 384 / info.bytesPerBlock,
+ _success: p.method === 'WriteTexture',
+ },
+ // When bytesPerRow is smaller than bytesInLastRow copying should fail.
+ {
+ bytesPerRow: 256,
+ widthInBlocks: (2 * 256) / info.bytesPerBlock,
+ copyWidthInBlocks: (2 * 256) / info.bytesPerBlock,
+ _success: false,
+ },
+ // When copyHeightInBlocks > 1, bytesPerRow must be specified.
+ {
+ bytesPerRow: undefined,
+ widthInBlocks: 256 / info.bytesPerBlock,
+ copyWidthInBlocks: 256 / info.bytesPerBlock,
+ _success: !(p.copyHeightInBlocks > 1 || p.copyDepth > 1),
+ },
+ ];
+ })
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ method,
+ format,
+ bytesPerRow,
+ widthInBlocks,
+ copyWidthInBlocks,
+ copyHeightInBlocks,
+ copyDepth,
+ _success,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ // We create an aligned texture using the widthInBlocks which may be different from the
+ // copyWidthInBlocks. This allows us to test scenarios where the two may be different.
+ const texture = t.createAlignedTexture(format, {
+ width: widthInBlocks * info.blockWidth,
+ height: copyHeightInBlocks * info.blockHeight,
+ depthOrArrayLayers: copyDepth,
+ });
+
+ const layout = { bytesPerRow, rowsPerImage: copyHeightInBlocks };
+ const copySize = {
+ width: copyWidthInBlocks * info.blockWidth,
+ height: copyHeightInBlocks * info.blockHeight,
+ depthOrArrayLayers: copyDepth,
+ };
+ const { minDataSizeOrOverestimate } = dataBytesForCopyOrOverestimate({
+ layout,
+ format,
+ copySize,
+ method,
+ });
+
+ t.testRun({ texture }, layout, copySize, {
+ dataSize: minDataSizeOrOverestimate,
+ method,
+ success: _success,
+ });
+ });
+
+g.test('bound_on_offset')
+ .desc(
+ `
+Test that the offset cannot be larger than the linear data size (even for an empty copy).
+- for various offsets and data sizes
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .beginSubcases()
+ .combine('offsetInBlocks', [0, 1, 2])
+ .combine('dataSizeInBlocks', [0, 1, 2])
+ )
+ .fn(async t => {
+ const { offsetInBlocks, dataSizeInBlocks, method } = t.params;
+
+ const format = 'rgba8unorm';
+ const info = kTextureFormatInfo[format];
+ const offset = offsetInBlocks * info.bytesPerBlock;
+ const dataSize = dataSizeInBlocks * info.bytesPerBlock;
+
+ const texture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const success = offset <= dataSize;
+
+ t.testRun(
+ { texture },
+ { offset, bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize, method, success }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts
new file mode 100644
index 0000000000..7d40b2d490
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/image_copy/texture_related.spec.ts
@@ -0,0 +1,538 @@
+export const description = `Texture related validation tests for B2T copy and T2B copy and writeTexture.`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert } from '../../../../common/util/util.js';
+import {
+ kColorTextureFormats,
+ kSizedTextureFormats,
+ kTextureDimensions,
+ kTextureFormatInfo,
+ kTextureUsages,
+ textureDimensionAndFormatCompatible,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { kResourceStates } from '../../../gpu_test.js';
+import { align } from '../../../util/math.js';
+import { virtualMipSize } from '../../../util/texture/base.js';
+import { kImageCopyTypes } from '../../../util/texture/layout.js';
+
+import {
+ ImageCopyTest,
+ texelBlockAlignmentTestExpanderForValueToCoordinate,
+ formatCopyableWithMethod,
+ getACopyableAspectWithMethod,
+} from './image_copy.js';
+
+export const g = makeTestGroup(ImageCopyTest);
+
+g.test('valid')
+ .desc(
+ `
+Test that the texture must be valid and not destroyed.
+- for all copy methods
+- for all texture states
+- for various dimensions
+`
+ )
+ .params(u =>
+ u //
+ .combine('method', kImageCopyTypes)
+ .combine('textureState', kResourceStates)
+ .combineWithParams([
+ { dimension: '1d', size: [4, 1, 1] },
+ { dimension: '2d', size: [4, 4, 1] },
+ { dimension: '2d', size: [4, 4, 3] },
+ { dimension: '3d', size: [4, 4, 3] },
+ ] as const)
+ )
+ .fn(async t => {
+ const { method, textureState, size, dimension } = t.params;
+
+ const texture = t.createTextureWithState(textureState, {
+ size,
+ dimension,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const success = textureState === 'valid';
+ const submit = textureState !== 'invalid';
+
+ t.testRun(
+ { texture },
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 1, method, success, submit }
+ );
+ });
+
+g.test('texture,device_mismatch')
+ .desc('Tests the image copies cannot be called with a texture created from another device')
+ .paramsSubcasesOnly(u =>
+ u.combine('method', kImageCopyTypes).combine('mismatched', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { method, mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const texture = sourceDevice.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ t.testRun(
+ { texture },
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 1, method, success: !mismatched }
+ );
+ });
+
+g.test('usage')
+ .desc(
+ `
+The texture must have the appropriate COPY_SRC/COPY_DST usage.
+- for various copy methods
+- for various dimensions
+- for various usages
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combineWithParams([
+ { dimension: '1d', size: [4, 1, 1] },
+ { dimension: '2d', size: [4, 4, 1] },
+ { dimension: '2d', size: [4, 4, 3] },
+ { dimension: '3d', size: [4, 4, 3] },
+ ] as const)
+ .beginSubcases()
+ // If usage0 and usage1 are the same, the usage being test is a single usage. Otherwise, it's
+ // a combined usage.
+ .combine('usage0', kTextureUsages)
+ .combine('usage1', kTextureUsages)
+ // RENDER_ATTACHMENT is not valid with 1d and 3d textures.
+ .unless(
+ ({ usage0, usage1, dimension }) =>
+ ((usage0 | usage1) & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
+ (dimension === '1d' || dimension === '3d')
+ )
+ )
+ .fn(async t => {
+ const { usage0, usage1, method, size, dimension } = t.params;
+
+ const usage = usage0 | usage1;
+ const texture = t.device.createTexture({
+ size,
+ dimension,
+ format: 'rgba8unorm',
+ usage,
+ });
+
+ const success =
+ method === 'CopyT2B'
+ ? (usage & GPUTextureUsage.COPY_SRC) !== 0
+ : (usage & GPUTextureUsage.COPY_DST) !== 0;
+
+ t.testRun(
+ { texture },
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 1, method, success }
+ );
+ });
+
+g.test('sample_count')
+ .desc(
+ `
+Test that multisampled textures cannot be copied.
+- for various copy methods
+- multisampled or not
+
+Note: we don't test 1D, 2D array and 3D textures because multisample is not supported them.
+`
+ )
+ .params(u =>
+ u //
+ .combine('method', kImageCopyTypes)
+ .beginSubcases()
+ .combine('sampleCount', [1, 4])
+ )
+ .fn(async t => {
+ const { sampleCount, method } = t.params;
+
+ const texture = t.device.createTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ sampleCount,
+ format: 'rgba8unorm',
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const success = sampleCount === 1;
+
+ t.testRun(
+ { texture },
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 1, method, success }
+ );
+ });
+
+g.test('mip_level')
+ .desc(
+ `
+Test that the mipLevel of the copy must be in range of the texture.
+- for various copy methods
+- for various dimensions
+- for several mipLevelCounts
+- for several target/source mipLevels`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combineWithParams([
+ { dimension: '1d', size: [32, 1, 1] },
+ { dimension: '2d', size: [32, 32, 1] },
+ { dimension: '2d', size: [32, 32, 3] },
+ { dimension: '3d', size: [32, 32, 3] },
+ ] as const)
+ .beginSubcases()
+ .combine('mipLevelCount', [1, 3, 5])
+ .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1)
+ .combine('mipLevel', [0, 1, 3, 4])
+ )
+ .fn(async t => {
+ const { mipLevelCount, mipLevel, method, size, dimension } = t.params;
+
+ const texture = t.device.createTexture({
+ size,
+ dimension,
+ mipLevelCount,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ const success = mipLevel < mipLevelCount;
+
+ t.testRun(
+ { texture, mipLevel },
+ { bytesPerRow: 0 },
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { dataSize: 1, method, success }
+ );
+ });
+
+g.test('format')
+ .desc(
+ `
+Test the copy must be a full subresource if the texture's format is depth/stencil format.
+- for various copy methods
+- for various dimensions
+- for all sized formats
+- for a couple target/source mipLevels
+- for some modifier (or not) for the full copy size
+`
+ )
+ .params(u =>
+ u //
+ .combine('method', kImageCopyTypes)
+ .combineWithParams([
+ { depthOrArrayLayers: 1, dimension: '1d' },
+ { depthOrArrayLayers: 1, dimension: '2d' },
+ { depthOrArrayLayers: 3, dimension: '2d' },
+ { depthOrArrayLayers: 32, dimension: '3d' },
+ ] as const)
+ .combine('format', kSizedTextureFormats)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .filter(formatCopyableWithMethod)
+ .beginSubcases()
+ .combine('mipLevel', [0, 2])
+ .unless(p => p.dimension === '1d' && p.mipLevel !== 0)
+ .combine('copyWidthModifier', [0, -1])
+ .combine('copyHeightModifier', [0, -1])
+ // If the texture has multiple depth/array slices and it is not a 3D texture, which means it is an array texture,
+ // depthModifier is not needed upon the third dimension. Because different layers are different subresources in
+ // an array texture. Whether it is a full copy or non-full copy doesn't make sense across different subresources.
+ // However, different depth slices on the same mip level are within the same subresource for a 3d texture. So we
+ // need to examine depth dimension via copyDepthModifier to determine whether it is a full copy for a 3D texture.
+ .expand('copyDepthModifier', ({ dimension: d }) => (d === '3d' ? [0, -1] : [0]))
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ method,
+ depthOrArrayLayers,
+ dimension,
+ format,
+ mipLevel,
+ copyWidthModifier,
+ copyHeightModifier,
+ copyDepthModifier,
+ } = t.params;
+
+ const info = kTextureFormatInfo[format];
+ const size = { width: 32 * info.blockWidth, height: 32 * info.blockHeight, depthOrArrayLayers };
+ if (dimension === '1d') {
+ size.height = 1;
+ }
+
+ const texture = t.device.createTexture({
+ size,
+ dimension,
+ format,
+ mipLevelCount: dimension === '1d' ? 1 : 5,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ let success = true;
+ if (
+ (info.depth || info.stencil) &&
+ (copyWidthModifier !== 0 || copyHeightModifier !== 0 || copyDepthModifier !== 0)
+ ) {
+ success = false;
+ }
+
+ const levelSize = virtualMipSize(
+ dimension,
+ [size.width, size.height, size.depthOrArrayLayers],
+ mipLevel
+ );
+ const copySize = [
+ levelSize[0] + copyWidthModifier * info.blockWidth,
+ levelSize[1] + copyHeightModifier * info.blockHeight,
+ // Note that compressed format is not supported for 3D textures yet, so there is no info.blockDepth.
+ levelSize[2] + copyDepthModifier,
+ ];
+
+ t.testRun(
+ { texture, mipLevel, aspect: getACopyableAspectWithMethod({ format, method }) },
+ { bytesPerRow: 512, rowsPerImage: 32 },
+ copySize,
+ {
+ dataSize: 512 * 32 * 32,
+ method,
+ success,
+ }
+ );
+ });
+
+g.test('origin_alignment')
+ .desc(
+ `
+Test that the texture copy origin must be aligned to the format's block size.
+- for various copy methods
+- for all color formats (depth stencil formats require a full copy)
+- for X, Y and Z coordinates
+- for various values for that coordinate depending on the block size
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ // No need to test depth/stencil formats because its copy origin must be [0, 0, 0], which is already aligned with block size.
+ .combine('format', kColorTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combineWithParams([
+ { depthOrArrayLayers: 1, dimension: '1d' },
+ { depthOrArrayLayers: 1, dimension: '2d' },
+ { depthOrArrayLayers: 3, dimension: '2d' },
+ { depthOrArrayLayers: 3, dimension: '3d' },
+ ] as const)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('coordinateToTest', ['x', 'y', 'z'] as const)
+ .unless(p => p.dimension === '1d' && p.coordinateToTest !== 'x')
+ .expand('valueToCoordinate', texelBlockAlignmentTestExpanderForValueToCoordinate)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const {
+ valueToCoordinate,
+ coordinateToTest,
+ format,
+ method,
+ depthOrArrayLayers,
+ dimension,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+ const size = { width: 0, height: 0, depthOrArrayLayers };
+ const origin = { x: 0, y: 0, z: 0 };
+ let success = true;
+
+ origin[coordinateToTest] = valueToCoordinate;
+ switch (coordinateToTest) {
+ case 'x': {
+ success = origin.x % info.blockWidth === 0;
+ break;
+ }
+ case 'y': {
+ success = origin.y % info.blockHeight === 0;
+ break;
+ }
+ }
+
+ const texture = t.createAlignedTexture(format, size, origin, dimension);
+
+ t.testRun({ texture, origin }, { bytesPerRow: 0, rowsPerImage: 0 }, size, {
+ dataSize: 1,
+ method,
+ success,
+ });
+ });
+
+g.test('size_alignment')
+ .desc(
+ `
+Test that the copy size must be aligned to the texture's format's block size.
+- for various copy methods
+- for all formats (depth-stencil formats require a full copy)
+- for all texture dimensions
+- for the size's parameters to test (width / height / depth)
+- for various values for that copy size parameters, depending on the block size
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ // No need to test depth/stencil formats because its copy size must be subresource's size, which is already aligned with block size.
+ .combine('format', kColorTextureFormats)
+ .filter(formatCopyableWithMethod)
+ .combine('dimension', kTextureDimensions)
+ .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format))
+ .beginSubcases()
+ .combine('coordinateToTest', ['width', 'height', 'depthOrArrayLayers'] as const)
+ .unless(p => p.dimension === '1d' && p.coordinateToTest !== 'width')
+ .expand('valueToCoordinate', texelBlockAlignmentTestExpanderForValueToCoordinate)
+ )
+ .beforeAllSubcases(t => {
+ const info = kTextureFormatInfo[t.params.format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { valueToCoordinate, coordinateToTest, dimension, format, method } = t.params;
+ const info = kTextureFormatInfo[format];
+ const size = { width: 0, height: 0, depthOrArrayLayers: 0 };
+ const origin = { x: 0, y: 0, z: 0 };
+ let success = true;
+
+ size[coordinateToTest] = valueToCoordinate;
+ switch (coordinateToTest) {
+ case 'width': {
+ success = size.width % info.blockWidth === 0;
+ break;
+ }
+ case 'height': {
+ success = size.height % info.blockHeight === 0;
+ break;
+ }
+ }
+
+ const texture = t.createAlignedTexture(format, size, origin, dimension);
+
+ const bytesPerRow = align(
+ Math.max(1, Math.ceil(size.width / info.blockWidth)) * info.bytesPerBlock,
+ 256
+ );
+ const rowsPerImage = Math.ceil(size.height / info.blockHeight);
+ t.testRun({ texture, origin }, { bytesPerRow, rowsPerImage }, size, {
+ dataSize: 1,
+ method,
+ success,
+ });
+ });
+
+g.test('copy_rectangle')
+ .desc(
+ `
+Test that the max corner of the copy rectangle (origin+copySize) must be inside the texture.
+- for various copy methods
+- for all dimensions
+- for the X, Y and Z dimensions
+- for various origin and copy size values (and texture sizes)
+- for various mip levels
+`
+ )
+ .params(u =>
+ u
+ .combine('method', kImageCopyTypes)
+ .combine('dimension', kTextureDimensions)
+ .beginSubcases()
+ .combine('originValue', [7, 8])
+ .combine('copySizeValue', [7, 8])
+ .combine('textureSizeValue', [14, 15])
+ .combine('mipLevel', [0, 2])
+ .combine('coordinateToTest', [0, 1, 2] as const)
+ .unless(p => p.dimension === '1d' && (p.coordinateToTest !== 0 || p.mipLevel !== 0))
+ )
+ .fn(async t => {
+ const {
+ originValue,
+ copySizeValue,
+ textureSizeValue,
+ mipLevel,
+ coordinateToTest,
+ method,
+ dimension,
+ } = t.params;
+ const format = 'rgba8unorm';
+ const info = kTextureFormatInfo[format];
+
+ const origin = [0, 0, 0];
+ const copySize = [0, 0, 0];
+ const textureSize = { width: 16 << mipLevel, height: 16 << mipLevel, depthOrArrayLayers: 16 };
+ if (dimension === '1d') {
+ textureSize.height = 1;
+ textureSize.depthOrArrayLayers = 1;
+ }
+ const success = originValue + copySizeValue <= textureSizeValue;
+
+ origin[coordinateToTest] = originValue;
+ copySize[coordinateToTest] = copySizeValue;
+ switch (coordinateToTest) {
+ case 0: {
+ textureSize.width = textureSizeValue << mipLevel;
+ break;
+ }
+ case 1: {
+ textureSize.height = textureSizeValue << mipLevel;
+ break;
+ }
+ case 2: {
+ textureSize.depthOrArrayLayers =
+ dimension === '3d' ? textureSizeValue << mipLevel : textureSizeValue;
+ break;
+ }
+ }
+
+ const texture = t.device.createTexture({
+ size: textureSize,
+ dimension,
+ mipLevelCount: dimension === '1d' ? 1 : 3,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ });
+
+ assert(copySize[0] % info.blockWidth === 0);
+ const bytesPerRow = align(copySize[0] / info.blockWidth, 256);
+ assert(copySize[1] % info.blockHeight === 0);
+ const rowsPerImage = copySize[1] / info.blockHeight;
+ t.testRun({ texture, origin, mipLevel }, { bytesPerRow, rowsPerImage }, copySize, {
+ dataSize: 1,
+ method,
+ success,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts
new file mode 100644
index 0000000000..986fc42296
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/layout_shader_compat.spec.ts
@@ -0,0 +1,14 @@
+export const description = `
+TODO:
+- interface matching between pipeline layout and shader
+ - x= {compute, vertex, fragment, vertex+fragment}, visibilities
+ - x= bind group index values, binding index values, multiple bindings
+ - x= types of bindings
+ - x= {equal, superset, subset}
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+import { ValidationTest } from './validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/create.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/create.spec.ts
new file mode 100644
index 0000000000..2c25a3561f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/create.spec.ts
@@ -0,0 +1,34 @@
+export const description = `
+Tests for validation in createQuerySet.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kQueryTypes, kMaxQueryCount } from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('count')
+ .desc(
+ `
+Tests that create query set with the count for all query types:
+- count {<, =, >} kMaxQueryCount
+- x= {occlusion, timestamp} query
+ `
+ )
+ .params(u =>
+ u
+ .combine('type', kQueryTypes)
+ .beginSubcases()
+ .combine('count', [0, kMaxQueryCount, kMaxQueryCount + 1])
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForQueryTypeOrSkipTestCase(t.params.type);
+ })
+ .fn(async t => {
+ const { type, count } = t.params;
+
+ t.expectValidationError(() => {
+ t.device.createQuerySet({ type, count });
+ }, count > kMaxQueryCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/destroy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/destroy.spec.ts
new file mode 100644
index 0000000000..64b6f4f606
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/query_set/destroy.spec.ts
@@ -0,0 +1,15 @@
+export const description = `
+Destroying a query set more than once is allowed.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('twice').fn(async t => {
+ const qset = t.device.createQuerySet({ type: 'occlusion', count: 1 });
+
+ qset.destroy();
+ qset.destroy();
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/README.txt
new file mode 100644
index 0000000000..a46a0e3d1c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/README.txt
@@ -0,0 +1,13 @@
+Tests for validation that occurs inside queued operations
+(submit, writeBuffer, writeTexture, copyExternalImageToTexture).
+
+BufferMapStatesToTest = {
+ mapped -> unmapped,
+ mapped at creation -> unmapped,
+ mapping pending -> unmapped,
+ pending -> mapped (await map),
+ unmapped -> pending (noawait map),
+ created mapped-at-creation,
+}
+
+Note writeTexture is tested in image_copy.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/buffer_mapped.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/buffer_mapped.spec.ts
new file mode 100644
index 0000000000..f979dc3146
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/buffer_mapped.spec.ts
@@ -0,0 +1,280 @@
+export const description = `
+Validation tests for the map-state of mappable buffers used in submitted command buffers.
+
+Tests every operation that has a dependency on a buffer
+ - writeBuffer
+ - copyB2B {src,dst}
+ - copyB2T
+ - copyT2B
+
+Test those operations against buffers in the following states:
+ - Unmapped
+ - In the process of mapping
+ - mapped
+ - mapped with a mapped range queried
+ - unmapped after mapping
+ - mapped at creation
+
+Also tests every order of operations combination of mapping operations and command recording
+operations to ensure the mapping state is only considered when a command buffer is submitted.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ async runBufferDependencyTest(usage: number, callback: Function): Promise<void> {
+ const bufferDesc = {
+ size: 8,
+ usage,
+ mappedAtCreation: false,
+ };
+
+ const mapMode = usage & GPUBufferUsage.MAP_READ ? GPUMapMode.READ : GPUMapMode.WRITE;
+
+ // Create a mappable buffer, and one that will remain unmapped for comparison.
+ const mappableBuffer = this.device.createBuffer(bufferDesc);
+ const unmappedBuffer = this.device.createBuffer(bufferDesc);
+
+ // Run the given operation before the buffer is mapped. Should succeed.
+ callback(mappableBuffer);
+
+ // Map the buffer
+ const mapPromise = mappableBuffer.mapAsync(mapMode);
+
+ // Run the given operation while the buffer is in the process of mapping. Should fail.
+ this.expectValidationError(() => {
+ callback(mappableBuffer);
+ });
+
+ // Run on a different, unmapped buffer. Should succeed.
+ callback(unmappedBuffer);
+
+ await mapPromise;
+
+ // Run the given operation when the buffer is finished mapping with no getMappedRange. Should fail.
+ this.expectValidationError(() => {
+ callback(mappableBuffer);
+ });
+
+ // Run on a different, unmapped buffer. Should succeed.
+ callback(unmappedBuffer);
+
+ // Run the given operation when the buffer is mapped with getMappedRange. Should fail.
+ mappableBuffer.getMappedRange();
+ this.expectValidationError(() => {
+ callback(mappableBuffer);
+ });
+
+ // Unmap the buffer and run the operation. Should succeed.
+ mappableBuffer.unmap();
+ callback(mappableBuffer);
+
+ // Create a buffer that's mappedAtCreation.
+ bufferDesc.mappedAtCreation = true;
+ const mappedBuffer = this.device.createBuffer(bufferDesc);
+
+ // Run the operation with the mappedAtCreation buffer. Should fail.
+ this.expectValidationError(() => {
+ callback(mappedBuffer);
+ });
+
+ // Run on a different, unmapped buffer. Should succeed.
+ callback(unmappedBuffer);
+
+ // Unmap the mappedAtCreation buffer and run the operation. Should succeed.
+ mappedBuffer.unmap();
+ callback(mappedBuffer);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('writeBuffer')
+ .desc(`Test that an outstanding mapping will prevent writeBuffer calls.`)
+ .fn(async t => {
+ const data = new Uint32Array([42]);
+
+ await t.runBufferDependencyTest(
+ GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ (buffer: GPUBuffer) => {
+ t.queue.writeBuffer(buffer, 0, data);
+ }
+ );
+ });
+
+g.test('copyBufferToBuffer')
+ .desc(
+ `
+ Test that an outstanding mapping will prevent copyBufferToTexture commands from submitting,
+ both when used as the source and destination.`
+ )
+ .fn(async t => {
+ const sourceBuffer = t.device.createBuffer({
+ size: 8,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+
+ const destBuffer = t.device.createBuffer({
+ size: 8,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ await t.runBufferDependencyTest(
+ GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
+ (buffer: GPUBuffer) => {
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.copyBufferToBuffer(buffer, 0, destBuffer, 0, 4);
+ t.queue.submit([commandEncoder.finish()]);
+ }
+ );
+
+ await t.runBufferDependencyTest(
+ GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ (buffer: GPUBuffer) => {
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.copyBufferToBuffer(sourceBuffer, 0, buffer, 0, 4);
+ t.queue.submit([commandEncoder.finish()]);
+ }
+ );
+ });
+
+g.test('copyBufferToTexture')
+ .desc(
+ `Test that an outstanding mapping will prevent copyBufferToTexture commands from submitting.`
+ )
+ .fn(async t => {
+ const size = { width: 1, height: 1 };
+
+ const texture = t.device.createTexture({
+ size,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ await t.runBufferDependencyTest(
+ GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
+ (buffer: GPUBuffer) => {
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.copyBufferToTexture({ buffer }, { texture }, size);
+ t.queue.submit([commandEncoder.finish()]);
+ }
+ );
+ });
+
+g.test('copyTextureToBuffer')
+ .desc(
+ `Test that an outstanding mapping will prevent copyTextureToBuffer commands from submitting.`
+ )
+ .fn(async t => {
+ const size = { width: 1, height: 1 };
+
+ const texture = t.device.createTexture({
+ size,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_SRC,
+ });
+
+ await t.runBufferDependencyTest(
+ GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ (buffer: GPUBuffer) => {
+ const commandEncoder = t.device.createCommandEncoder();
+ commandEncoder.copyTextureToBuffer({ texture }, { buffer }, size);
+ t.queue.submit([commandEncoder.finish()]);
+ }
+ );
+ });
+
+g.test('map_command_recording_order')
+ .desc(
+ `
+Test that the order of mapping a buffer relative to when commands are recorded that use it
+ does not matter, as long as the buffer is unmapped when the commands are submitted.
+ `
+ )
+ .paramsSubcasesOnly([
+ {
+ order: ['record', 'map', 'unmap', 'finish', 'submit'],
+ mappedAtCreation: false,
+ _shouldError: false,
+ },
+ {
+ order: ['record', 'map', 'finish', 'unmap', 'submit'],
+ mappedAtCreation: false,
+ _shouldError: false,
+ },
+ {
+ order: ['record', 'finish', 'map', 'unmap', 'submit'],
+ mappedAtCreation: false,
+ _shouldError: false,
+ },
+ {
+ order: ['map', 'record', 'unmap', 'finish', 'submit'],
+ mappedAtCreation: false,
+ _shouldError: false,
+ },
+ {
+ order: ['map', 'record', 'finish', 'unmap', 'submit'],
+ mappedAtCreation: false,
+ _shouldError: false,
+ },
+ {
+ order: ['map', 'record', 'finish', 'submit', 'unmap'],
+ mappedAtCreation: false,
+ _shouldError: true,
+ },
+ {
+ order: ['record', 'map', 'finish', 'submit', 'unmap'],
+ mappedAtCreation: false,
+ _shouldError: true,
+ },
+ {
+ order: ['record', 'finish', 'map', 'submit', 'unmap'],
+ mappedAtCreation: false,
+ _shouldError: true,
+ },
+ { order: ['record', 'unmap', 'finish', 'submit'], mappedAtCreation: true, _shouldError: false },
+ { order: ['record', 'finish', 'unmap', 'submit'], mappedAtCreation: true, _shouldError: false },
+ { order: ['record', 'finish', 'submit', 'unmap'], mappedAtCreation: true, _shouldError: true },
+ ] as const)
+ .fn(async t => {
+ const { order, mappedAtCreation, _shouldError: shouldError } = t.params;
+
+ const buffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
+ mappedAtCreation,
+ });
+
+ const targetBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ let commandBuffer: GPUCommandBuffer;
+
+ const steps = {
+ record: async () => {
+ commandEncoder.copyBufferToBuffer(buffer, 0, targetBuffer, 0, 4);
+ },
+ map: async () => {
+ await buffer.mapAsync(GPUMapMode.WRITE);
+ },
+ unmap: async () => {
+ buffer.unmap();
+ },
+ finish: async () => {
+ commandBuffer = commandEncoder.finish();
+ },
+ submit: async () => {
+ t.expectValidationError(() => {
+ t.queue.submit([commandBuffer]);
+ }, shouldError);
+ },
+ };
+
+ for (const op of order) {
+ await steps[op]();
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts
new file mode 100644
index 0000000000..1bb221f4b7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/copyToTexture/CopyExternalImageToTexture.spec.ts
@@ -0,0 +1,904 @@
+export const description = `
+copyExternalImageToTexture Validation Tests in Queue.
+Note that we don't need to add tests on the destination texture dimension as currently we require
+the destination texture should have RENDER_ATTACHMENT usage, which is only allowed to be used on 2D
+textures.
+`;
+
+import {
+ getResourcePath,
+ getCrossOriginResourcePath,
+} from '../../../../../common/framework/resources.js';
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { raceWithRejectOnTimeout, unreachable, assert } from '../../../../../common/util/util.js';
+import {
+ kTextureFormatInfo,
+ kTextureFormats,
+ kTextureUsages,
+ kValidTextureFormatsForCopyE2T,
+} from '../../../../capability_info.js';
+import { kResourceStates } from '../../../../gpu_test.js';
+import {
+ CanvasType,
+ canCopyFromCanvasContext,
+ createCanvas,
+ createOnscreenCanvas,
+ createOffscreenCanvas,
+ kValidCanvasContextIds,
+} from '../../../../util/create_elements.js';
+import { ValidationTest } from '../../validation_test.js';
+
+const kDefaultBytesPerPixel = 4; // using 'bgra8unorm' or 'rgba8unorm'
+const kDefaultWidth = 32;
+const kDefaultHeight = 32;
+const kDefaultDepth = 1;
+const kDefaultMipLevelCount = 6;
+
+function computeMipMapSize(width: number, height: number, mipLevel: number) {
+ return {
+ mipWidth: Math.max(width >> mipLevel, 1),
+ mipHeight: Math.max(height >> mipLevel, 1),
+ };
+}
+
+interface WithMipLevel {
+ mipLevel: number;
+}
+
+interface WithDstOriginMipLevel extends WithMipLevel {
+ dstOrigin: Required<GPUOrigin3DDict>;
+}
+
+// Helper function to generate copySize for src OOB test
+function generateCopySizeForSrcOOB({ srcOrigin }: { srcOrigin: Required<GPUOrigin2DDict> }) {
+ // OOB origin fails even with no-op copy.
+ if (srcOrigin.x > kDefaultWidth || srcOrigin.y > kDefaultHeight) {
+ return [{ width: 0, height: 0, depthOrArrayLayers: 0 }];
+ }
+
+ const justFitCopySize = {
+ width: kDefaultWidth - srcOrigin.x,
+ height: kDefaultHeight - srcOrigin.y,
+ depthOrArrayLayers: 1,
+ };
+
+ return [
+ justFitCopySize, // correct size, maybe no-op copy.
+ {
+ width: justFitCopySize.width + 1,
+ height: justFitCopySize.height,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers,
+ }, // OOB in width
+ {
+ width: justFitCopySize.width,
+ height: justFitCopySize.height + 1,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers,
+ }, // OOB in height
+ {
+ width: justFitCopySize.width,
+ height: justFitCopySize.height,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers + 1,
+ }, // OOB in depthOrArrayLayers
+ ];
+}
+
+// Helper function to generate dst origin value based on mipLevel.
+function generateDstOriginValue({ mipLevel }: WithMipLevel) {
+ const origin = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
+
+ return [
+ { x: 0, y: 0, z: 0 },
+ { x: origin.mipWidth - 1, y: 0, z: 0 },
+ { x: 0, y: origin.mipHeight - 1, z: 0 },
+ { x: origin.mipWidth, y: 0, z: 0 },
+ { x: 0, y: origin.mipHeight, z: 0 },
+ { x: 0, y: 0, z: kDefaultDepth },
+ { x: origin.mipWidth + 1, y: 0, z: 0 },
+ { x: 0, y: origin.mipHeight + 1, z: 0 },
+ { x: 0, y: 0, z: kDefaultDepth + 1 },
+ ];
+}
+
+// Helper function to generate copySize for dst OOB test
+function generateCopySizeForDstOOB({ mipLevel, dstOrigin }: WithDstOriginMipLevel) {
+ const dstMipMapSize = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
+
+ // OOB origin fails even with no-op copy.
+ if (
+ dstOrigin.x > dstMipMapSize.mipWidth ||
+ dstOrigin.y > dstMipMapSize.mipHeight ||
+ dstOrigin.z > kDefaultDepth
+ ) {
+ return [{ width: 0, height: 0, depthOrArrayLayers: 0 }];
+ }
+
+ const justFitCopySize = {
+ width: dstMipMapSize.mipWidth - dstOrigin.x,
+ height: dstMipMapSize.mipHeight - dstOrigin.y,
+ depthOrArrayLayers: kDefaultDepth - dstOrigin.z,
+ };
+
+ return [
+ justFitCopySize,
+ {
+ width: justFitCopySize.width + 1,
+ height: justFitCopySize.height,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers,
+ }, // OOB in width
+ {
+ width: justFitCopySize.width,
+ height: justFitCopySize.height + 1,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers,
+ }, // OOB in height
+ {
+ width: justFitCopySize.width,
+ height: justFitCopySize.height,
+ depthOrArrayLayers: justFitCopySize.depthOrArrayLayers + 1,
+ }, // OOB in depthOrArrayLayers
+ ];
+}
+
+class CopyExternalImageToTextureTest extends ValidationTest {
+ onlineCrossOriginUrl = 'https://raw.githubusercontent.com/gpuweb/gpuweb/main/logo/webgpu.png';
+
+ getImageData(width: number, height: number): ImageData {
+ if (typeof ImageData === 'undefined') {
+ this.skip('ImageData is not supported.');
+ }
+
+ const pixelSize = kDefaultBytesPerPixel * width * height;
+ const imagePixels = new Uint8ClampedArray(pixelSize);
+ return new ImageData(imagePixels, width, height);
+ }
+
+ getCanvasWithContent(
+ canvasType: CanvasType,
+ width: number,
+ height: number,
+ content: HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap
+ ): HTMLCanvasElement | OffscreenCanvas {
+ const canvas = createCanvas(this, canvasType, 1, 1);
+ const ctx = canvas.getContext('2d');
+ assert(ctx !== null);
+ ctx.drawImage(content, 0, 0);
+
+ return canvas;
+ }
+
+ createImageBitmap(image: ImageBitmapSource | OffscreenCanvas): Promise<ImageBitmap> {
+ if (typeof createImageBitmap === 'undefined') {
+ this.skip('Creating ImageBitmaps is not supported.');
+ }
+ return createImageBitmap(image);
+ }
+
+ runTest(
+ imageBitmapCopyView: GPUImageCopyExternalImage,
+ textureCopyView: GPUImageCopyTextureTagged,
+ copySize: GPUExtent3D,
+ validationScopeSuccess: boolean,
+ exceptionName?: string
+ ): void {
+ // copyExternalImageToTexture will generate two types of errors. One is synchronous exceptions;
+ // the other is asynchronous validation error scope errors.
+ if (exceptionName) {
+ this.shouldThrow(exceptionName, () => {
+ this.device.queue.copyExternalImageToTexture(
+ imageBitmapCopyView,
+ textureCopyView,
+ copySize
+ );
+ });
+ } else {
+ this.expectValidationError(() => {
+ this.device.queue.copyExternalImageToTexture(
+ imageBitmapCopyView,
+ textureCopyView,
+ copySize
+ );
+ }, !validationScopeSuccess);
+ }
+ }
+}
+
+export const g = makeTestGroup(CopyExternalImageToTextureTest);
+
+g.test('source_canvas,contexts')
+ .desc(
+ `
+ Test HTMLCanvasElement as source image with different contexts.
+
+ Call HTMLCanvasElement.getContext() with different context type.
+ Only '2d', 'experimental-webgl', 'webgl', 'webgl2' is valid context
+ type.
+
+ Check whether 'OperationError' is generated when context type is invalid.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('contextType', kValidCanvasContextIds)
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { contextType, copySize } = t.params;
+ const canvas = createOnscreenCanvas(t, 1, 1);
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const ctx = canvas.getContext(contextType);
+ if (ctx === null) {
+ t.skip('Failed to get context for canvas element');
+ return;
+ }
+ t.tryTrackForCleanup(ctx);
+
+ t.runTest(
+ { source: canvas },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ canCopyFromCanvasContext(contextType) ? '' : 'OperationError'
+ );
+ });
+
+g.test('source_offscreenCanvas,contexts')
+ .desc(
+ `
+ Test OffscreenCanvas as source image with different contexts.
+
+ Call OffscreenCanvas.getContext() with different context type.
+ Only '2d', 'webgl', 'webgl2', 'webgpu' is valid context type.
+
+ Check whether 'OperationError' is generated when context type is invalid.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('contextType', kValidCanvasContextIds)
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { contextType, copySize } = t.params;
+ const canvas = createOffscreenCanvas(t, 1, 1);
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
+ // `OffscreenCanvas.getContext` that takes `string` or a union of context types.
+ const ctx = ((canvas as unknown) as HTMLCanvasElement).getContext(contextType);
+
+ if (ctx === null) {
+ t.skip('Failed to get context for canvas element');
+ return;
+ }
+ t.tryTrackForCleanup(ctx);
+
+ t.runTest(
+ { source: canvas },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ canCopyFromCanvasContext(contextType) ? '' : 'OperationError'
+ );
+ });
+
+g.test('source_image,crossOrigin')
+ .desc(
+ `
+ Test contents of source image is [clean, cross-origin].
+
+ Load crossOrigin image or same origin image and init the source
+ images.
+
+ Check whether 'SecurityError' is generated when source image is not origin clean.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('sourceImage', ['canvas', 'offscreenCanvas', 'imageBitmap'])
+ .combine('isOriginClean', [true, false])
+ .beginSubcases()
+ .combine('contentFrom', ['image', 'imageBitmap', 'canvas', 'offscreenCanvas'] as const)
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { sourceImage, isOriginClean, contentFrom, copySize } = t.params;
+ if (typeof document === 'undefined') {
+ t.skip('DOM is not available to create an image element.');
+ }
+
+ const crossOriginUrl = getCrossOriginResourcePath('webgpu.png', t.onlineCrossOriginUrl);
+ const originCleanUrl = getResourcePath('webgpu.png');
+ const img = document.createElement('img');
+ img.src = isOriginClean ? originCleanUrl : crossOriginUrl;
+
+ // Load image
+ const timeout_ms = 5000;
+ try {
+ await raceWithRejectOnTimeout(img.decode(), timeout_ms, 'load image timeout');
+ } catch (e) {
+ if (isOriginClean) {
+ throw e;
+ } else {
+ t.skip('Cannot load cross origin image in time');
+ return;
+ }
+ }
+
+ // The externalImage contents can be updated by:
+ // - decoded image element
+ // - canvas/offscreenCanvas with image draw on it.
+ // - imageBitmap created with the image.
+ // Test covers all of these cases to ensure origin clean checks works.
+ let source: HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap;
+ switch (contentFrom) {
+ case 'image': {
+ source = img;
+ break;
+ }
+ case 'imageBitmap': {
+ source = await t.createImageBitmap(img);
+ break;
+ }
+ case 'canvas':
+ case 'offscreenCanvas': {
+ const canvasType = contentFrom === 'offscreenCanvas' ? 'offscreen' : 'onscreen';
+ source = t.getCanvasWithContent(canvasType, 1, 1, img);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ // Update the externalImage content with source.
+ let externalImage: HTMLCanvasElement | OffscreenCanvas | ImageBitmap;
+ switch (sourceImage) {
+ case 'imageBitmap': {
+ externalImage = await t.createImageBitmap(source);
+ break;
+ }
+ case 'canvas':
+ case 'offscreenCanvas': {
+ const canvasType = contentFrom === 'offscreenCanvas' ? 'offscreen' : 'onscreen';
+ externalImage = t.getCanvasWithContent(canvasType, 1, 1, source);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ t.runTest(
+ { source: externalImage },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ isOriginClean ? '' : 'SecurityError'
+ );
+ });
+
+g.test('source_imageBitmap,state')
+ .desc(
+ `
+ Test ImageBitmap as source image in state [valid, closed].
+
+ Call imageBitmap.close() to transfer the imageBitmap into
+ 'closed' state.
+
+ Check whether 'InvalidStateError' is generated when ImageBitmap is
+ closed.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('closed', [false, true])
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { closed, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ if (closed) imageBitmap.close();
+
+ t.runTest(
+ { source: imageBitmap },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ closed ? 'InvalidStateError' : ''
+ );
+ });
+
+g.test('source_canvas,state')
+ .desc(
+ `
+ Test HTMLCanvasElement as source image in state
+ [nocontext, 'placeholder-nocontext', 'placeholder-hascontext', valid].
+
+ Nocontext means using a canvas without any context as copy param.
+
+ Call 'transferControlToOffscreen' on HTMLCanvasElement will cause the
+ canvas control right transfer. And this canvas is in state 'placeholder'
+ Whether getContext in new generated offscreenCanvas won't affect the origin
+ canvas state.
+
+
+ Check whether 'OperationError' is generated when HTMLCanvasElement has no
+ context.
+
+ Check whether 'InvalidStateError' is generated when HTMLCanvasElement is
+ in 'placeholder' state.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('state', ['nocontext', 'placeholder-nocontext', 'placeholder-hascontext', 'valid'])
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { state, copySize } = t.params;
+ const canvas = createOnscreenCanvas(t, 1, 1);
+ if (typeof canvas.transferControlToOffscreen === 'undefined') {
+ t.skip("Browser doesn't support HTMLCanvasElement.transferControlToOffscreen");
+ return;
+ }
+
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ let exceptionName: string = '';
+
+ switch (state) {
+ case 'nocontext': {
+ exceptionName = 'OperationError';
+ break;
+ }
+ case 'placeholder-nocontext': {
+ canvas.transferControlToOffscreen();
+ exceptionName = 'InvalidStateError';
+ break;
+ }
+ case 'placeholder-hascontext': {
+ const offscreenCanvas = canvas.transferControlToOffscreen();
+ t.tryTrackForCleanup(offscreenCanvas.getContext('webgl'));
+ exceptionName = 'InvalidStateError';
+ break;
+ }
+ case 'valid': {
+ assert(canvas.getContext('2d') !== null);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ t.runTest(
+ { source: canvas },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ exceptionName
+ );
+ });
+
+g.test('source_offscreenCanvas,state')
+ .desc(
+ `
+ Test OffscreenCanvas as source image in state [valid, detached].
+
+ Nocontext means using a canvas without any context as copy param.
+
+ Transfer OffscreenCanvas with MessageChannel will detach the OffscreenCanvas.
+
+ Check whether 'OperationError' is generated when HTMLCanvasElement has no
+ context.
+
+ Check whether 'InvalidStateError' is generated when OffscreenCanvas is
+ detached.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('state', ['nocontext', 'detached-nocontext', 'detached-hascontext', 'valid'])
+ .beginSubcases()
+ .combine('getContextInOffscreenCanvas', [false, true])
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { state, copySize } = t.params;
+ const offscreenCanvas = createOffscreenCanvas(t, 1, 1);
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ let exceptionName: string = '';
+ switch (state) {
+ case 'nocontext': {
+ exceptionName = 'OperationError';
+ break;
+ }
+ case 'detached-nocontext': {
+ const messageChannel = new MessageChannel();
+ messageChannel.port1.postMessage(offscreenCanvas, [offscreenCanvas]);
+
+ exceptionName = 'InvalidStateError';
+ break;
+ }
+ case 'detached-hascontext': {
+ const messageChannel = new MessageChannel();
+ const port2FirstMessage = new Promise(resolve => {
+ messageChannel.port2.onmessage = m => resolve(m);
+ });
+
+ messageChannel.port1.postMessage(offscreenCanvas, [offscreenCanvas]);
+
+ const receivedOffscreenCanvas = (await port2FirstMessage) as MessageEvent;
+ t.tryTrackForCleanup(receivedOffscreenCanvas.data.getContext('webgl'));
+
+ exceptionName = 'InvalidStateError';
+ break;
+ }
+ case 'valid': {
+ offscreenCanvas.getContext('webgl');
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ t.runTest(
+ { source: offscreenCanvas },
+ { texture: dstTexture },
+ copySize,
+ true, // No validation errors.
+ exceptionName
+ );
+ });
+
+g.test('destination_texture,state')
+ .desc(
+ `
+ Test dst texture is [valid, invalid, destroyed].
+
+ Check that an error is generated when texture is an error texture.
+ Check that an error is generated when texture is in destroyed state.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('state', kResourceStates)
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { state, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+ const dstTexture = t.createTextureWithState(state);
+
+ t.runTest({ source: imageBitmap }, { texture: dstTexture }, copySize, state === 'valid');
+ });
+
+g.test('destination_texture,device_mismatch')
+ .desc(
+ 'Tests copyExternalImageToTexture cannot be called with a destination texture created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+ const copySize = { width: 1, height: 1, depthOrArrayLayers: 1 };
+
+ const texture = sourceDevice.createTexture({
+ size: copySize,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+
+ t.runTest({ source: imageBitmap }, { texture }, copySize, !mismatched);
+ });
+
+g.test('destination_texture,usage')
+ .desc(
+ `
+ Test dst texture usages
+
+ Check that an error is generated when texture is created without usage COPY_DST | RENDER_ATTACHMENT.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('usage', kTextureUsages)
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { usage, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage,
+ });
+
+ t.runTest(
+ { source: imageBitmap },
+ { texture: dstTexture },
+ copySize,
+ !!(usage & GPUTextureUsage.COPY_DST && usage & GPUTextureUsage.RENDER_ATTACHMENT)
+ );
+ });
+
+g.test('destination_texture,sample_count')
+ .desc(
+ `
+ Test dst texture sample count.
+
+ Check that an error is generated when sample count it not 1.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('sampleCount', [1, 4])
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { sampleCount, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ sampleCount,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ t.runTest({ source: imageBitmap }, { texture: dstTexture }, copySize, sampleCount === 1);
+ });
+
+g.test('destination_texture,mipLevel')
+ .desc(
+ `
+ Test dst mipLevel.
+
+ Check that an error is generated when mipLevel is too large.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('mipLevel', [0, kDefaultMipLevelCount - 1, kDefaultMipLevelCount])
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .fn(async t => {
+ const { mipLevel, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+ const dstTexture = t.device.createTexture({
+ size: { width: kDefaultWidth, height: kDefaultHeight, depthOrArrayLayers: kDefaultDepth },
+ mipLevelCount: kDefaultMipLevelCount,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ t.runTest(
+ { source: imageBitmap },
+ { texture: dstTexture, mipLevel },
+ copySize,
+ mipLevel < kDefaultMipLevelCount
+ );
+ });
+
+g.test('destination_texture,format')
+ .desc(
+ `
+ Test dst texture format.
+
+ Check that an error is generated when texture format is not valid.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kTextureFormats)
+ .beginSubcases()
+ .combine('copySize', [
+ { width: 0, height: 0, depthOrArrayLayers: 0 },
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ ])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { format, copySize } = t.params;
+
+ const imageBitmap = await t.createImageBitmap(t.getImageData(1, 1));
+
+ // createTexture with all possible texture format may have validation error when using
+ // compressed texture format.
+ t.device.pushErrorScope('validation');
+ const dstTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ void t.device.popErrorScope();
+
+ const success = (kValidTextureFormatsForCopyE2T as readonly string[]).includes(format);
+
+ t.runTest({ source: imageBitmap }, { texture: dstTexture }, copySize, success);
+ });
+
+g.test('OOB,source')
+ .desc(
+ `
+ Test source image origin and copy size
+
+ Check that an error is generated when source.externalImage.origin + copySize is too large.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('srcOrigin', [
+ { x: 0, y: 0 }, // origin is on top-left
+ { x: kDefaultWidth - 1, y: 0 }, // x near the border
+ { x: 0, y: kDefaultHeight - 1 }, // y is near the border
+ { x: kDefaultWidth, y: kDefaultHeight }, // origin is on bottom-right
+ { x: kDefaultWidth + 1, y: 0 }, // x is too large
+ { x: 0, y: kDefaultHeight + 1 }, // y is too large
+ ])
+ .expand('copySize', generateCopySizeForSrcOOB)
+ )
+ .fn(async t => {
+ const { srcOrigin, copySize } = t.params;
+ const imageBitmap = await t.createImageBitmap(t.getImageData(kDefaultWidth, kDefaultHeight));
+ const dstTexture = t.device.createTexture({
+ size: {
+ width: kDefaultWidth + 1,
+ height: kDefaultHeight + 1,
+ depthOrArrayLayers: kDefaultDepth,
+ },
+ mipLevelCount: kDefaultMipLevelCount,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ let success = true;
+
+ if (
+ srcOrigin.x + copySize.width > kDefaultWidth ||
+ srcOrigin.y + copySize.height > kDefaultHeight ||
+ copySize.depthOrArrayLayers > 1
+ ) {
+ success = false;
+ }
+
+ t.runTest(
+ { source: imageBitmap, origin: srcOrigin },
+ { texture: dstTexture },
+ copySize,
+ true,
+ success ? '' : 'OperationError'
+ );
+ });
+
+g.test('OOB,destination')
+ .desc(
+ `
+ Test dst texture copy origin and copy size
+
+ Check that an error is generated when destination.texture.origin + copySize is too large.
+ Check that 'OperationError' is generated when copySize.depth is larger than 1.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('mipLevel', [0, 1, kDefaultMipLevelCount - 2])
+ .expand('dstOrigin', generateDstOriginValue)
+ .expand('copySize', generateCopySizeForDstOOB)
+ )
+ .fn(async t => {
+ const { mipLevel, dstOrigin, copySize } = t.params;
+
+ const imageBitmap = await t.createImageBitmap(
+ t.getImageData(kDefaultWidth + 1, kDefaultHeight + 1)
+ );
+ const dstTexture = t.device.createTexture({
+ size: {
+ width: kDefaultWidth,
+ height: kDefaultHeight,
+ depthOrArrayLayers: kDefaultDepth,
+ },
+ format: 'bgra8unorm',
+ mipLevelCount: kDefaultMipLevelCount,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ let success = true;
+ let hasOperationError = false;
+ const dstMipMapSize = computeMipMapSize(kDefaultWidth, kDefaultHeight, mipLevel);
+
+ if (
+ copySize.depthOrArrayLayers > 1 ||
+ dstOrigin.x + copySize.width > dstMipMapSize.mipWidth ||
+ dstOrigin.y + copySize.height > dstMipMapSize.mipHeight ||
+ dstOrigin.z + copySize.depthOrArrayLayers > kDefaultDepth
+ ) {
+ success = false;
+ }
+ if (copySize.depthOrArrayLayers > 1) {
+ hasOperationError = true;
+ }
+
+ t.runTest(
+ { source: imageBitmap },
+ {
+ texture: dstTexture,
+ mipLevel,
+ origin: dstOrigin,
+ },
+ copySize,
+ success,
+ hasOperationError ? 'OperationError' : ''
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts
new file mode 100644
index 0000000000..87d85d10ee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/destroyed/query_set.spec.ts
@@ -0,0 +1,63 @@
+export const description = `
+Tests using a destroyed query set on a queue.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { ValidationTest } from '../../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('beginOcclusionQuery')
+ .desc(
+ `
+Tests that use a destroyed query set in occlusion query on render pass encoder.
+- x= {destroyed, not destroyed (control case)}
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('querySetState', ['valid', 'destroyed'] as const))
+ .fn(t => {
+ const occlusionQuerySet = t.createQuerySetWithState(t.params.querySetState);
+
+ const encoder = t.createEncoder('render pass', { occlusionQuerySet });
+ encoder.encoder.beginOcclusionQuery(0);
+ encoder.encoder.endOcclusionQuery();
+ encoder.validateFinishAndSubmitGivenState(t.params.querySetState);
+ });
+
+g.test('writeTimestamp')
+ .desc(
+ `
+Tests that use a destroyed query set in writeTimestamp on {non-pass, compute, render} encoder.
+- x= {destroyed, not destroyed (control case)}
+ `
+ )
+ .params(u => u.beginSubcases().combine('querySetState', ['valid', 'destroyed'] as const))
+ .beforeAllSubcases(t => t.selectDeviceOrSkipTestCase('timestamp-query'))
+ .fn(async t => {
+ const querySet = t.createQuerySetWithState(t.params.querySetState, {
+ type: 'timestamp',
+ count: 2,
+ });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.writeTimestamp(querySet, 0);
+ encoder.validateFinishAndSubmitGivenState(t.params.querySetState);
+ });
+
+g.test('resolveQuerySet')
+ .desc(
+ `
+Tests that use a destroyed query set in resolveQuerySet.
+- x= {destroyed, not destroyed (control case)}
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('querySetState', ['valid', 'destroyed'] as const))
+ .fn(async t => {
+ const querySet = t.createQuerySetWithState(t.params.querySetState);
+
+ const buffer = t.device.createBuffer({ size: 8, usage: GPUBufferUsage.QUERY_RESOLVE });
+
+ const encoder = t.createEncoder('non-pass');
+ encoder.encoder.resolveQuerySet(querySet, 0, 1, buffer, 0);
+ encoder.validateFinishAndSubmitGivenState(t.params.querySetState);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/submit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/submit.spec.ts
new file mode 100644
index 0000000000..74afc6cf31
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/submit.spec.ts
@@ -0,0 +1,47 @@
+export const description = `
+Tests submit validation.
+
+Note: destroyed buffer/texture/querySet are tested in destroyed/. (unless it gets moved here)
+Note: buffer map state is tested in ./buffer_mapped.spec.ts.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('command_buffer,device_mismatch')
+ .desc(
+ `
+ Tests submit cannot be called with command buffers created from another device
+ Test with two command buffers to make sure all command buffers can be validated:
+ - cb0 and cb1 from same device
+ - cb0 and cb1 from different device
+ `
+ )
+ .paramsSubcasesOnly([
+ { cb0Mismatched: false, cb1Mismatched: false }, // control case
+ { cb0Mismatched: true, cb1Mismatched: false },
+ { cb0Mismatched: false, cb1Mismatched: true },
+ ])
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { cb0Mismatched, cb1Mismatched } = t.params;
+ const mismatched = cb0Mismatched || cb1Mismatched;
+
+ const encoder0 = cb0Mismatched
+ ? t.mismatchedDevice.createCommandEncoder()
+ : t.device.createCommandEncoder();
+ const cb0 = encoder0.finish();
+
+ const encoder1 = cb1Mismatched
+ ? t.mismatchedDevice.createCommandEncoder()
+ : t.device.createCommandEncoder();
+ const cb1 = encoder1.finish();
+
+ t.expectValidationError(() => {
+ t.device.queue.submit([cb0, cb1]);
+ }, mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeBuffer.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeBuffer.spec.ts
new file mode 100644
index 0000000000..120c5afe5f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeBuffer.spec.ts
@@ -0,0 +1,200 @@
+export const description = `
+Tests writeBuffer validation.
+
+Note: buffer map state is tested in ./buffer_mapped.spec.ts.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ kTypedArrayBufferViewConstructors,
+ TypedArrayBufferView,
+ TypedArrayBufferViewConstructor,
+} from '../../../../common/util/util.js';
+import { Float16Array } from '../../../../external/petamoriken/float16/float16.js';
+import { GPUConst } from '../../../constants.js';
+import { kResourceStates } from '../../../gpu_test.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('buffer_state')
+ .desc(
+ `
+ Test that the buffer used for GPUQueue.writeBuffer() must be valid. Tests calling writeBuffer
+ with {valid, invalid, destroyed} buffer.
+ `
+ )
+ .params(u => u.combine('bufferState', kResourceStates))
+ .fn(async t => {
+ const { bufferState } = t.params;
+ const buffer = t.createBufferWithState(bufferState, {
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ const data = new Uint8Array(16);
+ const _valid = bufferState === 'valid';
+
+ t.expectValidationError(() => {
+ t.device.queue.writeBuffer(buffer, 0, data, 0, data.length);
+ }, !_valid);
+ });
+
+g.test('ranges')
+ .desc(
+ `
+ Tests that the data ranges given to GPUQueue.writeBuffer() are properly validated. Tests calling
+ writeBuffer with both TypedArrays and ArrayBuffers and checks that the data offset and size is
+ interpreted correctly for both.
+ - When passing a TypedArray the data offset and size is given in elements.
+ - When passing an ArrayBuffer the data offset and size is given in bytes.
+
+ Also verifies that the specified data range:
+ - Describes a valid range of the destination buffer and source buffer.
+ - Fits fully within the destination buffer.
+ - Has a byte size which is a multiple of 4.
+ `
+ )
+ .fn(async t => {
+ const queue = t.device.queue;
+
+ function runTest(arrayType: TypedArrayBufferViewConstructor, testBuffer: boolean) {
+ const elementSize = arrayType.BYTES_PER_ELEMENT;
+ const bufferSize = 16 * elementSize;
+ const buffer = t.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ const arraySm: TypedArrayBufferView | ArrayBuffer = testBuffer
+ ? new arrayType(8).buffer
+ : new arrayType(8);
+ const arrayMd: TypedArrayBufferView | ArrayBuffer = testBuffer
+ ? new arrayType(16).buffer
+ : new arrayType(16);
+ const arrayLg: TypedArrayBufferView | ArrayBuffer = testBuffer
+ ? new arrayType(32).buffer
+ : new arrayType(32);
+
+ if (elementSize < 4) {
+ const array15: TypedArrayBufferView | ArrayBuffer = testBuffer
+ ? new arrayType(15).buffer
+ : new arrayType(15);
+
+ // Writing the full buffer that isn't 4-byte aligned.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, array15));
+
+ // Writing from an offset that causes source to be 4-byte aligned.
+ queue.writeBuffer(buffer, 0, array15, 3);
+
+ // Writing from an offset that causes the source to not be 4-byte aligned.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arrayMd, 3));
+
+ // Writing with a size that is not 4-byte aligned.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, 0, 7));
+ }
+
+ // Writing the full buffer without offsets.
+ queue.writeBuffer(buffer, 0, arraySm);
+ queue.writeBuffer(buffer, 0, arrayMd);
+ t.expectValidationError(() => queue.writeBuffer(buffer, 0, arrayLg));
+
+ // Writing the full buffer with a 4-byte aligned offset.
+ queue.writeBuffer(buffer, 8, arraySm);
+ t.expectValidationError(() => queue.writeBuffer(buffer, 8, arrayMd));
+
+ // Writing the full buffer with a unaligned offset.
+ t.expectValidationError(() => queue.writeBuffer(buffer, 3, arraySm));
+
+ // Writing remainder of buffer from offset.
+ queue.writeBuffer(buffer, 0, arraySm, 4);
+ queue.writeBuffer(buffer, 0, arrayMd, 4);
+ t.expectValidationError(() => queue.writeBuffer(buffer, 0, arrayLg, 4));
+
+ // Writing a larger buffer from an offset that allows it to fit in the destination.
+ queue.writeBuffer(buffer, 0, arrayLg, 16);
+
+ // Writing with both an offset and size.
+ queue.writeBuffer(buffer, 0, arraySm, 4, 4);
+
+ // Writing with a size that extends past the source buffer length.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, 0, 16));
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, 4, 8));
+
+ // Writing with a size that is 4-byte aligned but an offset that is not.
+ queue.writeBuffer(buffer, 0, arraySm, 3, 4);
+
+ // Writing zero bytes at the end of the buffer.
+ queue.writeBuffer(buffer, bufferSize, arraySm, 0, 0);
+
+ // Writing with a buffer offset that is out of range of buffer size.
+ t.expectValidationError(() => queue.writeBuffer(buffer, bufferSize + 4, arraySm, 0, 0));
+
+ // Writing zero bytes from the end of the data.
+ queue.writeBuffer(buffer, 0, arraySm, 8, 0);
+
+ // Writing with a data offset that is out of range of data size.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, 9, 0));
+
+ // Writing with a data offset that is out of range of data size with implicit copy size.
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, 9, undefined));
+
+ // A data offset of undefined should be treated as 0.
+ queue.writeBuffer(buffer, 0, arraySm, undefined, 8);
+ t.shouldThrow('OperationError', () => queue.writeBuffer(buffer, 0, arraySm, undefined, 12));
+ }
+
+ runTest(Uint8Array, true);
+
+ for (const arrayType of kTypedArrayBufferViewConstructors) {
+ if (arrayType === Float16Array) {
+ // Skip Float16Array since it is supplied by an external module, so there isn't an overload for it.
+ continue;
+ }
+ runTest(arrayType, false);
+ }
+ });
+
+g.test('usages')
+ .desc(
+ `
+ Tests calling writeBuffer with the buffer missed COPY_DST usage.
+ - buffer {with, without} COPY DST usage
+ `
+ )
+ .paramsSubcasesOnly([
+ { usage: GPUConst.BufferUsage.COPY_DST, _valid: true }, // control case
+ { usage: GPUConst.BufferUsage.STORAGE, _valid: false }, // without COPY_DST usage
+ { usage: GPUConst.BufferUsage.STORAGE | GPUConst.BufferUsage.COPY_SRC, _valid: false }, // with other usage
+ { usage: GPUConst.BufferUsage.STORAGE | GPUConst.BufferUsage.COPY_DST, _valid: true }, // with COPY_DST usage
+ ])
+ .fn(async t => {
+ const { usage, _valid } = t.params;
+ const buffer = t.device.createBuffer({ size: 16, usage });
+ const data = new Uint8Array(16);
+
+ t.expectValidationError(() => {
+ t.device.queue.writeBuffer(buffer, 0, data, 0, data.length);
+ }, !_valid);
+ });
+
+g.test('buffer,device_mismatch')
+ .desc('Tests writeBuffer cannot be called with a buffer created from another device.')
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const buffer = sourceDevice.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ t.trackForCleanup(buffer);
+
+ const data = new Uint8Array(16);
+
+ t.expectValidationError(() => {
+ t.device.queue.writeBuffer(buffer, 0, data, 0, data.length);
+ }, mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeTexture.spec.ts
new file mode 100644
index 0000000000..de9d3ef154
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/queue/writeTexture.spec.ts
@@ -0,0 +1,110 @@
+export const description = `Tests writeTexture validation.`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../constants.js';
+import { kResourceStates } from '../../../gpu_test.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('texture_state')
+ .desc(
+ `
+ Test that the texture used for GPUQueue.writeTexture() must be valid. Tests calling writeTexture
+ with {valid, invalid, destroyed} texture.
+ `
+ )
+ .params(u => u.combine('textureState', kResourceStates))
+ .fn(async t => {
+ const { textureState } = t.params;
+ const texture = t.createTextureWithState(textureState);
+ const data = new Uint8Array(16);
+ const size = [1, 1];
+
+ const isValid = textureState === 'valid';
+
+ t.expectValidationError(() => {
+ t.device.queue.writeTexture({ texture }, data, {}, size);
+ }, !isValid);
+ });
+
+g.test('usages')
+ .desc(
+ `
+ Tests calling writeTexture with the texture missed COPY_DST usage.
+ - texture {with, without} COPY DST usage
+ `
+ )
+ .paramsSubcasesOnly([
+ { usage: GPUConst.TextureUsage.COPY_DST }, // control case
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.COPY_SRC },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.COPY_DST },
+ ])
+ .fn(async t => {
+ const { usage } = t.params;
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16 },
+ usage,
+ format: 'rgba8unorm' as const,
+ });
+ const data = new Uint8Array(16);
+ const size = [1, 1];
+
+ const isValid = usage & GPUConst.TextureUsage.COPY_DST ? true : false;
+ t.expectValidationError(() => {
+ t.device.queue.writeTexture({ texture }, data, {}, size);
+ }, !isValid);
+ });
+
+g.test('sample_count')
+ .desc(
+ `
+ Test that the texture sample count. Check that a validation error is generated if sample count is
+ not 1.
+ `
+ )
+ .params(u => u.combine('sampleCount', [1, 4]))
+ .fn(async t => {
+ const { sampleCount } = t.params;
+ const texture = t.device.createTexture({
+ size: { width: 16, height: 16 },
+ sampleCount,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const data = new Uint8Array(16);
+ const size = [1, 1];
+
+ const isValid = sampleCount === 1;
+
+ t.expectValidationError(() => {
+ t.device.queue.writeTexture({ texture }, data, {}, size);
+ }, !isValid);
+ });
+
+g.test('texture,device_mismatch')
+ .desc('Tests writeTexture cannot be called with a texture created from another device.')
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const texture = sourceDevice.createTexture({
+ size: { width: 16, height: 16 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ t.trackForCleanup(texture);
+
+ const data = new Uint8Array(16);
+ const size = [1, 1];
+
+ t.expectValidationError(() => {
+ t.device.queue.writeTexture({ texture }, data, {}, size);
+ }, mismatched);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt
new file mode 100644
index 0000000000..a5797c2b63
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/README.txt
@@ -0,0 +1 @@
+Render pass stuff other than commands (which are in encoding/cmds/).
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts
new file mode 100644
index 0000000000..2883d3c1b9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/attachment_compatibility.spec.ts
@@ -0,0 +1,639 @@
+export const description = `
+Validation for attachment compatibility between render passes, bundles, and pipelines
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import {
+ kRegularTextureFormats,
+ kSizedDepthStencilFormats,
+ kUnsizedDepthStencilFormats,
+ kTextureSampleCounts,
+ kMaxColorAttachments,
+ kTextureFormatInfo,
+ getFeaturesForFormats,
+ filterFormatsByFeature,
+} from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+const kColorAttachmentCounts = range(kMaxColorAttachments, i => i + 1);
+const kColorAttachments = kColorAttachmentCounts
+ .map(count => {
+ // generate cases with 0..1 null attachments at different location
+ // e.g. count == 2
+ // [
+ // [1, 1],
+ // [0, 1],
+ // [1, 0],
+ // ]
+ // 0 (false) means null attachment, 1 (true) means non-null attachment, at the slot
+
+ // Special cases: we need at least a color attachment, when we don't have depth stencil attachment
+ if (count === 1) {
+ return [[1]];
+ }
+ if (count === 2) {
+ return [
+ [1, 1],
+ [0, 1],
+ [1, 0],
+ ];
+ }
+
+ // [1, 1, ..., 1]: all color attachment are used
+ let result = [new Array<boolean>(count).fill(true)];
+
+ // [1, 0, 1, ..., 1]: generate cases with one null attachment at different locations
+ result = result.concat(
+ range(count, i => {
+ const r = new Array<boolean>(count).fill(true);
+ r[i] = false;
+ return r;
+ })
+ );
+
+ // [1, 0, 1, ..., 0, 1]: generate cases with two null attachments at different locations
+ // To reduce test run time, limit the attachment count to <= 4
+ if (count <= 4) {
+ result = result.concat(
+ range(count - 1, i => {
+ const cases = [] as boolean[][];
+ for (let j = i + 1; j < count; j++) {
+ const r = new Array<boolean>(count).fill(true);
+ r[i] = false;
+ r[j] = false;
+ cases.push(r);
+ }
+ return cases;
+ }).flat()
+ );
+ }
+
+ return result;
+ })
+ .flat() as boolean[][];
+
+const kDepthStencilAttachmentFormats = [
+ undefined,
+ ...kSizedDepthStencilFormats,
+ ...kUnsizedDepthStencilFormats,
+] as const;
+
+const kFeaturesForDepthStencilAttachmentFormats = getFeaturesForFormats([
+ ...kSizedDepthStencilFormats,
+ ...kUnsizedDepthStencilFormats,
+]);
+
+class F extends ValidationTest {
+ createAttachmentTextureView(format: GPUTextureFormat, sampleCount?: number) {
+ return this.device
+ .createTexture({
+ // Size matching the "arbitrary" size used by ValidationTest helpers.
+ size: [16, 16, 1],
+ format,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount,
+ })
+ .createView();
+ }
+
+ createColorAttachment(
+ format: GPUTextureFormat | null,
+ sampleCount?: number
+ ): GPURenderPassColorAttachment | null {
+ return format === null
+ ? null
+ : {
+ view: this.createAttachmentTextureView(format, sampleCount),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ };
+ }
+
+ createDepthAttachment(
+ format: GPUTextureFormat,
+ sampleCount?: number
+ ): GPURenderPassDepthStencilAttachment {
+ const attachment: GPURenderPassDepthStencilAttachment = {
+ view: this.createAttachmentTextureView(format, sampleCount),
+ };
+ if (kTextureFormatInfo[format].depth) {
+ attachment.depthClearValue = 0;
+ attachment.depthLoadOp = 'clear';
+ attachment.depthStoreOp = 'discard';
+ }
+ if (kTextureFormatInfo[format].stencil) {
+ attachment.stencilClearValue = 1;
+ attachment.stencilLoadOp = 'clear';
+ attachment.stencilStoreOp = 'discard';
+ }
+ return attachment;
+ }
+
+ createRenderPipeline(
+ targets: Iterable<GPUColorTargetState | null>,
+ depthStencil?: GPUDepthStencilState,
+ sampleCount?: number,
+ cullMode?: GPUCullMode
+ ) {
+ return this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: '@fragment fn main() {}',
+ }),
+ entryPoint: 'main',
+ targets,
+ },
+ primitive: { topology: 'triangle-list', cullMode },
+ depthStencil,
+ multisample: { count: sampleCount },
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kColorAttachmentFormats = kRegularTextureFormats.filter(format => {
+ const info = kTextureFormatInfo[format];
+ return info.color && info.renderable;
+});
+
+g.test('render_pass_and_bundle,color_format')
+ .desc('Test that color attachment formats in render passes and bundles must match.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('passFormat', kColorAttachmentFormats)
+ .combine('bundleFormat', kColorAttachmentFormats)
+ )
+ .fn(t => {
+ const { passFormat, bundleFormat } = t.params;
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: [bundleFormat],
+ });
+ const bundle = bundleEncoder.finish();
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [t.createColorAttachment(passFormat)],
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(passFormat === bundleFormat, true);
+ });
+
+g.test('render_pass_and_bundle,color_count')
+ .desc(
+ `
+ Test that the number of color attachments in render passes and bundles must match.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('passCount', kColorAttachmentCounts)
+ .combine('bundleCount', kColorAttachmentCounts)
+ )
+ .fn(t => {
+ const { passCount, bundleCount } = t.params;
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: range(bundleCount, () => 'rgba8uint'),
+ });
+ const bundle = bundleEncoder.finish();
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: range(passCount, () => t.createColorAttachment('rgba8uint')),
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(passCount === bundleCount, true);
+ });
+
+g.test('render_pass_and_bundle,color_sparse')
+ .desc(
+ `
+ Test that each of color attachments in render passes and bundles must match.
+ `
+ )
+ .params(u =>
+ u //
+ // introduce attachmentCount to make it easier to split the test
+ .combine('attachmentCount', kColorAttachmentCounts)
+ .beginSubcases()
+ .combine('passAttachments', kColorAttachments)
+ .combine('bundleAttachments', kColorAttachments)
+ .filter(
+ p =>
+ p.attachmentCount === p.passAttachments.length &&
+ p.attachmentCount === p.bundleAttachments.length
+ )
+ )
+ .fn(t => {
+ const { passAttachments, bundleAttachments } = t.params;
+ const colorFormats = bundleAttachments.map(i => (i ? 'rgba8uint' : null));
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const colorAttachments = passAttachments.map(i =>
+ t.createColorAttachment(i ? 'rgba8uint' : null)
+ );
+ const pass = encoder.beginRenderPass({
+ colorAttachments,
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(
+ passAttachments.every((v, i) => v === bundleAttachments[i]),
+ true
+ );
+ });
+
+g.test('render_pass_and_bundle,depth_format')
+ .desc('Test that the depth attachment format in render passes and bundles must match.')
+ .params(u =>
+ u //
+ .combine('passFeature', kFeaturesForDepthStencilAttachmentFormats)
+ .combine('bundleFeature', kFeaturesForDepthStencilAttachmentFormats)
+ .beginSubcases()
+ .expand('passFormat', ({ passFeature }) =>
+ filterFormatsByFeature(passFeature, kDepthStencilAttachmentFormats)
+ )
+ .expand('bundleFormat', ({ bundleFeature }) =>
+ filterFormatsByFeature(bundleFeature, kDepthStencilAttachmentFormats)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { passFeature, bundleFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([passFeature, bundleFeature]);
+ })
+ .fn(async t => {
+ const { passFormat, bundleFormat } = t.params;
+
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ depthStencilFormat: bundleFormat,
+ });
+ const bundle = bundleEncoder.finish();
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [t.createColorAttachment('rgba8unorm')],
+ depthStencilAttachment:
+ passFormat !== undefined ? t.createDepthAttachment(passFormat) : undefined,
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(passFormat === bundleFormat, true);
+ });
+
+g.test('render_pass_and_bundle,sample_count')
+ .desc('Test that the sample count in render passes and bundles must match.')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('renderSampleCount', kTextureSampleCounts)
+ .combine('bundleSampleCount', kTextureSampleCounts)
+ )
+ .fn(t => {
+ const { renderSampleCount, bundleSampleCount } = t.params;
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ sampleCount: bundleSampleCount,
+ });
+ const bundle = bundleEncoder.finish();
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [t.createColorAttachment('rgba8unorm', renderSampleCount)],
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(renderSampleCount === bundleSampleCount, true);
+ });
+
+g.test('render_pass_and_bundle,device_mismatch')
+ .desc('Test that render passes cannot be called with bundles created from another device.')
+ .paramsSubcasesOnly(u => u.combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(t => {
+ const { mismatched } = t.params;
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const format = 'r16float';
+ const bundleEncoder = sourceDevice.createRenderBundleEncoder({
+ colorFormats: [format],
+ });
+ const bundle = bundleEncoder.finish();
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder('non-pass');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [t.createColorAttachment(format)],
+ });
+ pass.executeBundles([bundle]);
+ pass.end();
+ validateFinishAndSubmit(!mismatched, true);
+ });
+
+g.test('render_pass_or_bundle_and_pipeline,color_format')
+ .desc(
+ `
+Test that color attachment formats in render passes or bundles match the pipeline color format.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ .beginSubcases()
+ .combine('encoderFormat', kColorAttachmentFormats)
+ .combine('pipelineFormat', kColorAttachmentFormats)
+ )
+ .fn(t => {
+ const { encoderType, encoderFormat, pipelineFormat } = t.params;
+ const pipeline = t.createRenderPipeline([{ format: pipelineFormat, writeMask: 0 }]);
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: { colorFormats: [encoderFormat] },
+ });
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmit(encoderFormat === pipelineFormat, true);
+ });
+
+g.test('render_pass_or_bundle_and_pipeline,color_count')
+ .desc(
+ `
+Test that the number of color attachments in render passes or bundles match the pipeline color
+count.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ .beginSubcases()
+ .combine('encoderCount', kColorAttachmentCounts)
+ .combine('pipelineCount', kColorAttachmentCounts)
+ )
+ .fn(t => {
+ const { encoderType, encoderCount, pipelineCount } = t.params;
+ const pipeline = t.createRenderPipeline(
+ range(pipelineCount, () => ({ format: 'rgba8uint', writeMask: 0 }))
+ );
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: { colorFormats: range(encoderCount, () => 'rgba8uint') },
+ });
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmit(encoderCount === pipelineCount, true);
+ });
+
+g.test('render_pass_or_bundle_and_pipeline,color_sparse')
+ .desc(
+ `
+Test that each of color attachments in render passes or bundles match that of the pipeline.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ // introduce attachmentCount to make it easier to split the test
+ .combine('attachmentCount', kColorAttachmentCounts)
+ .beginSubcases()
+ .combine('encoderAttachments', kColorAttachments)
+ .combine('pipelineAttachments', kColorAttachments)
+ .filter(
+ p =>
+ p.attachmentCount === p.encoderAttachments.length &&
+ p.attachmentCount === p.pipelineAttachments.length
+ )
+ )
+ .fn(t => {
+ const { encoderType, encoderAttachments, pipelineAttachments } = t.params;
+
+ const colorTargets = pipelineAttachments.map(i =>
+ i ? ({ format: 'rgba8uint', writeMask: 0 } as GPUColorTargetState) : null
+ );
+ const pipeline = t.createRenderPipeline(colorTargets);
+
+ const colorFormats = encoderAttachments.map(i => (i ? 'rgba8uint' : null));
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: { colorFormats },
+ });
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmit(
+ encoderAttachments.every((v, i) => v === pipelineAttachments[i]),
+ true
+ );
+ });
+
+g.test('render_pass_or_bundle_and_pipeline,depth_format')
+ .desc(
+ `
+Test that the depth attachment format in render passes or bundles match the pipeline depth format.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ .combine('encoderFormatFeature', kFeaturesForDepthStencilAttachmentFormats)
+ .combine('pipelineFormatFeature', kFeaturesForDepthStencilAttachmentFormats)
+ .beginSubcases()
+ .expand('encoderFormat', ({ encoderFormatFeature }) =>
+ filterFormatsByFeature(encoderFormatFeature, kDepthStencilAttachmentFormats)
+ )
+ .expand('pipelineFormat', ({ pipelineFormatFeature }) =>
+ filterFormatsByFeature(pipelineFormatFeature, kDepthStencilAttachmentFormats)
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { encoderFormatFeature, pipelineFormatFeature } = t.params;
+ t.selectDeviceOrSkipTestCase([encoderFormatFeature, pipelineFormatFeature]);
+ })
+ .fn(async t => {
+ const { encoderType, encoderFormat, pipelineFormat } = t.params;
+
+ const pipeline = t.createRenderPipeline(
+ [{ format: 'rgba8unorm', writeMask: 0 }],
+ pipelineFormat !== undefined ? { format: pipelineFormat } : undefined
+ );
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: { colorFormats: ['rgba8unorm'], depthStencilFormat: encoderFormat },
+ });
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmit(encoderFormat === pipelineFormat, true);
+ });
+
+const kStencilFaceStates = [
+ { failOp: 'keep', depthFailOp: 'keep', passOp: 'keep' },
+ { failOp: 'zero', depthFailOp: 'zero', passOp: 'zero' },
+] as GPUStencilFaceState[];
+
+g.test('render_pass_or_bundle_and_pipeline,depth_stencil_read_only_write_state')
+ .desc(
+ `
+Test that the depth stencil read only state in render passes or bundles is compatible with the depth stencil write state of the pipeline.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ .combine('format', kDepthStencilAttachmentFormats)
+ .beginSubcases()
+ // pass/bundle state
+ .combine('depthReadOnly', [false, true])
+ .combine('stencilReadOnly', [false, true])
+ .combine('stencilFront', kStencilFaceStates)
+ .combine('stencilBack', kStencilFaceStates)
+ // pipeline state
+ .combine('depthWriteEnabled', [false, true])
+ .combine('stencilWriteMask', [0, 0xffffffff])
+ .combine('cullMode', ['none', 'front', 'back'] as const)
+ .filter(p => {
+ if (p.format) {
+ const depthStencilInfo = kTextureFormatInfo[p.format];
+ // For combined depth/stencil formats the depth and stencil read only state must match
+ // in order to create a valid render bundle or render pass.
+ if (depthStencilInfo.depth && depthStencilInfo.stencil) {
+ if (p.depthReadOnly !== p.stencilReadOnly) {
+ return false;
+ }
+ }
+ // If the format has no depth aspect, the depthReadOnly, depthWriteEnabled of the pipeline must not be true
+ // in order to create a valid render pipeline.
+ if (!depthStencilInfo.depth && p.depthWriteEnabled) {
+ return false;
+ }
+ // If the format has no stencil aspect, the stencil state operation must be 'keep'
+ // in order to create a valid render pipeline.
+ if (
+ !depthStencilInfo.stencil &&
+ (p.stencilFront.failOp !== 'keep' || p.stencilBack.failOp !== 'keep')
+ ) {
+ return false;
+ }
+ }
+ // No depthStencil attachment
+ return true;
+ })
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const {
+ encoderType,
+ format,
+ depthReadOnly,
+ stencilReadOnly,
+ depthWriteEnabled,
+ stencilWriteMask,
+ cullMode,
+ stencilFront,
+ stencilBack,
+ } = t.params;
+
+ const pipeline = t.createRenderPipeline(
+ [{ format: 'rgba8unorm', writeMask: 0 }],
+ format === undefined
+ ? undefined
+ : {
+ format,
+ depthWriteEnabled,
+ stencilWriteMask,
+ stencilFront,
+ stencilBack,
+ },
+ 1,
+ cullMode
+ );
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: {
+ colorFormats: ['rgba8unorm'],
+ depthStencilFormat: format,
+ depthReadOnly,
+ stencilReadOnly,
+ },
+ });
+ encoder.setPipeline(pipeline);
+
+ let writesDepth = false;
+ let writesStencil = false;
+ if (format) {
+ writesDepth = depthWriteEnabled;
+ if (stencilWriteMask !== 0) {
+ if (
+ cullMode !== 'front' &&
+ (stencilFront.passOp !== 'keep' ||
+ stencilFront.depthFailOp !== 'keep' ||
+ stencilFront.failOp !== 'keep')
+ ) {
+ writesStencil = true;
+ }
+ if (
+ cullMode !== 'back' &&
+ (stencilBack.passOp !== 'keep' ||
+ stencilBack.depthFailOp !== 'keep' ||
+ stencilBack.failOp !== 'keep')
+ ) {
+ writesStencil = true;
+ }
+ }
+ }
+
+ let isValid = true;
+ if (writesDepth) {
+ isValid &&= !depthReadOnly;
+ }
+ if (writesStencil) {
+ isValid &&= !stencilReadOnly;
+ }
+
+ validateFinishAndSubmit(isValid, true);
+ });
+
+g.test('render_pass_or_bundle_and_pipeline,sample_count')
+ .desc(
+ `
+Test that the sample count in render passes or bundles match the pipeline sample count for both color texture and depthstencil texture.
+`
+ )
+ .params(u =>
+ u
+ .combine('encoderType', ['render pass', 'render bundle'] as const)
+ .combine('attachmentType', ['color', 'depthstencil'] as const)
+ .beginSubcases()
+ .combine('encoderSampleCount', kTextureSampleCounts)
+ .combine('pipelineSampleCount', kTextureSampleCounts)
+ )
+ .fn(t => {
+ const { encoderType, attachmentType, encoderSampleCount, pipelineSampleCount } = t.params;
+
+ const colorFormats = attachmentType === 'color' ? ['rgba8unorm' as const] : [];
+ const depthStencilFormat =
+ attachmentType === 'depthstencil' ? ('depth24plus-stencil8' as const) : undefined;
+
+ const pipeline = t.createRenderPipeline(
+ colorFormats.map(format => ({ format, writeMask: 0 })),
+ depthStencilFormat ? { format: depthStencilFormat } : undefined,
+ pipelineSampleCount
+ );
+
+ const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
+ attachmentInfo: { colorFormats, depthStencilFormat, sampleCount: encoderSampleCount },
+ });
+ encoder.setPipeline(pipeline);
+ validateFinishAndSubmit(encoderSampleCount === pipelineSampleCount, true);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts
new file mode 100644
index 0000000000..63c5fd20a4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/render_pass_descriptor.spec.ts
@@ -0,0 +1,1129 @@
+export const description = `
+render pass descriptor validation tests.
+
+TODO: review for completeness
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import {
+ kDepthStencilFormats,
+ kMaxColorAttachments,
+ kQueryTypes,
+ kRenderableColorTextureFormats,
+ kTextureFormatInfo,
+} from '../../../capability_info.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+class F extends ValidationTest {
+ createTexture(
+ options: {
+ format?: GPUTextureFormat;
+ width?: number;
+ height?: number;
+ arrayLayerCount?: number;
+ mipLevelCount?: number;
+ sampleCount?: number;
+ usage?: GPUTextureUsageFlags;
+ } = {}
+ ): GPUTexture {
+ const {
+ format = 'rgba8unorm',
+ width = 16,
+ height = 16,
+ arrayLayerCount = 1,
+ mipLevelCount = 1,
+ sampleCount = 1,
+ usage = GPUTextureUsage.RENDER_ATTACHMENT,
+ } = options;
+
+ return this.device.createTexture({
+ size: { width, height, depthOrArrayLayers: arrayLayerCount },
+ format,
+ mipLevelCount,
+ sampleCount,
+ usage,
+ });
+ }
+
+ getColorAttachment(
+ texture: GPUTexture,
+ textureViewDescriptor?: GPUTextureViewDescriptor
+ ): GPURenderPassColorAttachment {
+ const view = texture.createView(textureViewDescriptor);
+
+ return {
+ view,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ };
+ }
+
+ getDepthStencilAttachment(
+ texture: GPUTexture,
+ textureViewDescriptor?: GPUTextureViewDescriptor
+ ): GPURenderPassDepthStencilAttachment {
+ const view = texture.createView(textureViewDescriptor);
+
+ return {
+ view,
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ stencilClearValue: 0,
+ stencilLoadOp: 'clear',
+ stencilStoreOp: 'store',
+ };
+ }
+
+ tryRenderPass(success: boolean, descriptor: GPURenderPassDescriptor): void {
+ const commandEncoder = this.device.createCommandEncoder();
+ const renderPass = commandEncoder.beginRenderPass(descriptor);
+ renderPass.end();
+
+ this.expectValidationError(() => {
+ commandEncoder.finish();
+ }, !success);
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('attachments,one_color_attachment')
+ .desc(`Test that a render pass works with only one color attachment.`)
+ .fn(t => {
+ const colorTexture = t.createTexture({ format: 'rgba8unorm' });
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('attachments,one_depth_stencil_attachment')
+ .desc(`Test that a render pass works with only one depthStencil attachment.`)
+ .fn(t => {
+ const depthStencilTexture = t.createTexture({ format: 'depth24plus-stencil8' });
+ const descriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('color_attachments,empty')
+ .desc(
+ `
+ Test that when colorAttachments has all values be 'undefined' or the sequence is empty, the
+ depthStencilAttachment must not be 'undefined'.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('colorAttachments', [
+ [],
+ [undefined],
+ [undefined, undefined],
+ new Array(8).fill(undefined),
+ [{ format: 'rgba8unorm' }],
+ ])
+ .combine('hasDepthStencilAttachment', [false, true])
+ )
+ .fn(async t => {
+ const { colorAttachments, hasDepthStencilAttachment } = t.params;
+
+ let isEmptyColorTargets = true;
+ for (let i = 0; i < colorAttachments.length; i++) {
+ if (colorAttachments[i] !== undefined) {
+ isEmptyColorTargets = false;
+ const colorTexture = t.createTexture();
+ colorAttachments[i] = t.getColorAttachment(colorTexture);
+ }
+ }
+
+ const _success = !isEmptyColorTargets || hasDepthStencilAttachment;
+ t.tryRenderPass(_success, {
+ colorAttachments,
+ depthStencilAttachment: hasDepthStencilAttachment
+ ? t.getDepthStencilAttachment(t.createTexture({ format: 'depth24plus-stencil8' }))
+ : undefined,
+ });
+ });
+
+g.test('color_attachments,limits,maxColorAttachments')
+ .desc(
+ `
+ Test that the out of bound of color attachment indexes are handled.
+ - a validation error is generated when color attachments exceed the maximum limit(8).
+ `
+ )
+ .paramsSimple([
+ { colorAttachmentsCount: 8, _success: true }, // Control case
+ { colorAttachmentsCount: 9, _success: false }, // Out of bounds
+ ])
+ .fn(async t => {
+ const { colorAttachmentsCount, _success } = t.params;
+
+ const colorAttachments = [];
+ for (let i = 0; i < colorAttachmentsCount; i++) {
+ const colorTexture = t.createTexture();
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+
+ t.tryRenderPass(_success, { colorAttachments });
+ });
+
+g.test('color_attachments,limits,maxColorAttachmentBytesPerSample,aligned')
+ .desc(
+ `
+ Test that the total bytes per sample of the formats of the color attachments must be no greater
+ than maxColorAttachmentBytesPerSample when the components are aligned (same format).
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine(
+ 'attachmentCount',
+ range(kMaxColorAttachments, i => i + 1)
+ )
+ )
+ .fn(async t => {
+ const { format, attachmentCount } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const colorAttachments = [];
+ for (let i = 0; i < attachmentCount; i++) {
+ const colorTexture = t.createTexture();
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+ const shouldError =
+ info.renderTargetPixelByteCost === undefined ||
+ info.renderTargetPixelByteCost * attachmentCount >
+ t.device.limits.maxColorAttachmentBytesPerSample;
+
+ t.tryRenderPass(!shouldError, { colorAttachments });
+ });
+
+g.test('color_attachments,limits,maxColorAttachmentBytesPerSample,unaligned')
+ .desc(
+ `
+ Test that the total bytes per sample of the formats of the color attachments must be no greater
+ than maxColorAttachmentBytesPerSample when the components are (potentially) unaligned.
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ // Alignment causes the first 1 byte R8Unorm to become 4 bytes. So even though
+ // 1+4+8+16+1 < 32, the 4 byte alignment requirement of R32Float makes the first R8Unorm
+ // become 4 and 4+4+8+16+1 > 32. Re-ordering this so the R8Unorm's are at the end, however
+ // is allowed: 4+8+16+1+1 < 32.
+ {
+ formats: [
+ 'r8unorm',
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _success: true,
+ },
+ {
+ formats: [
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _success: false,
+ },
+ ])
+ )
+ .fn(async t => {
+ const { formats, _success } = t.params;
+
+ const colorAttachments = [];
+ for (const format of formats) {
+ const colorTexture = t.createTexture({ format });
+ colorAttachments.push(t.getColorAttachment(colorTexture));
+ }
+ t.tryRenderPass(_success, { colorAttachments });
+ });
+
+g.test('attachments,same_size')
+ .desc(
+ `
+ Test that attachments have the same size. Otherwise, a validation error should be generated.
+ - Succeed if all attachments have the same size.
+ - Fail if one of the color attachments has a different size.
+ - Fail if the depth stencil attachment has a different size.
+ `
+ )
+ .fn(async t => {
+ const colorTexture1x1A = t.createTexture({ width: 1, height: 1, format: 'rgba8unorm' });
+ const colorTexture1x1B = t.createTexture({ width: 1, height: 1, format: 'rgba8unorm' });
+ const colorTexture2x2 = t.createTexture({ width: 2, height: 2, format: 'rgba8unorm' });
+ const depthStencilTexture1x1 = t.createTexture({
+ width: 1,
+ height: 1,
+ format: 'depth24plus-stencil8',
+ });
+ const depthStencilTexture2x2 = t.createTexture({
+ width: 2,
+ height: 2,
+ format: 'depth24plus-stencil8',
+ });
+
+ {
+ // Control case: all the same size (1x1)
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture1x1B),
+ ],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture1x1),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // One of the color attachments has a different size
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture2x2),
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // The depth stencil attachment has a different size
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture1x1A),
+ t.getColorAttachment(colorTexture1x1B),
+ ],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture2x2),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('attachments,color_depth_mismatch')
+ .desc(`Test that attachments match whether they are used for color or depth stencil.`)
+ .fn(async t => {
+ const colorTexture = t.createTexture({ format: 'rgba8unorm' });
+ const depthStencilTexture = t.createTexture({ format: 'depth24plus-stencil8' });
+
+ {
+ // Using depth-stencil for color
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(depthStencilTexture)],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // Using color for depth-stencil
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(colorTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('attachments,layer_count')
+ .desc(
+ `
+ Test the layer counts for color or depth stencil.
+ - Fail if using 2D array texture view with arrayLayerCount > 1.
+ - Succeed if using 2D array texture view that covers the first layer of the texture.
+ - Succeed if using 2D array texture view that covers the last layer for depth stencil.
+ `
+ )
+ .paramsSimple([
+ { arrayLayerCount: 5, baseArrayLayer: 0, _success: false },
+ { arrayLayerCount: 1, baseArrayLayer: 0, _success: true },
+ { arrayLayerCount: 1, baseArrayLayer: 9, _success: true },
+ ])
+ .fn(async t => {
+ const { arrayLayerCount, baseArrayLayer, _success } = t.params;
+
+ const ARRAY_LAYER_COUNT = 10;
+ const MIP_LEVEL_COUNT = 1;
+ const COLOR_FORMAT = 'rgba8unorm';
+ const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
+
+ const colorTexture = t.createTexture({
+ format: COLOR_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+ const depthStencilTexture = t.createTexture({
+ format: DEPTH_STENCIL_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+
+ const baseTextureViewDescriptor: GPUTextureViewDescriptor = {
+ dimension: '2d-array',
+ baseArrayLayer,
+ arrayLayerCount,
+ baseMipLevel: 0,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ };
+
+ {
+ // Check 2D array texture view for color
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: COLOR_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture, textureViewDescriptor)],
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ {
+ // Check 2D array texture view for depth stencil
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: DEPTH_STENCIL_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(
+ depthStencilTexture,
+ textureViewDescriptor
+ ),
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ });
+
+g.test('attachments,mip_level_count')
+ .desc(
+ `
+ Test the mip level count for color or depth stencil.
+ - Fail if using 2D texture view with mipLevelCount > 1.
+ - Succeed if using 2D texture view that covers the first level of the texture.
+ - Succeed if using 2D texture view that covers the last level of the texture.
+ `
+ )
+ .paramsSimple([
+ { mipLevelCount: 2, baseMipLevel: 0, _success: false },
+ { mipLevelCount: 1, baseMipLevel: 0, _success: true },
+ { mipLevelCount: 1, baseMipLevel: 3, _success: true },
+ ])
+ .fn(async t => {
+ const { mipLevelCount, baseMipLevel, _success } = t.params;
+
+ const ARRAY_LAYER_COUNT = 1;
+ const MIP_LEVEL_COUNT = 4;
+ const COLOR_FORMAT = 'rgba8unorm';
+ const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
+
+ const colorTexture = t.createTexture({
+ format: COLOR_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+ const depthStencilTexture = t.createTexture({
+ format: DEPTH_STENCIL_FORMAT,
+ width: 32,
+ height: 32,
+ mipLevelCount: MIP_LEVEL_COUNT,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ });
+
+ const baseTextureViewDescriptor: GPUTextureViewDescriptor = {
+ dimension: '2d',
+ baseArrayLayer: 0,
+ arrayLayerCount: ARRAY_LAYER_COUNT,
+ baseMipLevel,
+ mipLevelCount,
+ };
+
+ {
+ // Check 2D texture view for color
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: COLOR_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture, textureViewDescriptor)],
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ {
+ // Check 2D texture view for depth stencil
+ const textureViewDescriptor: GPUTextureViewDescriptor = {
+ ...baseTextureViewDescriptor,
+ format: DEPTH_STENCIL_FORMAT,
+ };
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(
+ depthStencilTexture,
+ textureViewDescriptor
+ ),
+ };
+
+ t.tryRenderPass(_success, descriptor);
+ }
+ });
+
+g.test('color_attachments,non_multisampled')
+ .desc(
+ `
+ Test that setting a resolve target is invalid if the color attachments is non multisampled.
+ `
+ )
+ .fn(async t => {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const resolveTargetTexture = t.createTexture({ sampleCount: 1 });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ resolveTarget: resolveTargetTexture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('color_attachments,sample_count')
+ .desc(
+ `
+ Test the usages of multisampled textures for color attachments.
+ - Succeed if using a multisampled color attachment without setting a resolve target.
+ - Fail if using multiple color attachments with different sample counts.
+ `
+ )
+ .fn(async t => {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+
+ {
+ // It is allowed to use a multisampled color attachment without setting resolve target
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ };
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // It is not allowed to use multiple color attachments with different sample counts
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ t.getColorAttachment(colorTexture),
+ t.getColorAttachment(multisampledColorTexture),
+ ],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ });
+
+g.test('resolveTarget,sample_count')
+ .desc(
+ `
+ Test that using multisampled resolve target is invalid for color attachments.
+ `
+ )
+ .fn(async t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const multisampledResolveTargetTexture = t.createTexture({ sampleCount: 4 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = multisampledResolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,array_layer_count')
+ .desc(
+ `
+ Test that using a resolve target with array layer count is greater than 1 is invalid for color
+ attachments.
+ `
+ )
+ .fn(async t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ arrayLayerCount: 2 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView({ dimension: '2d-array' });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,mipmap_level_count')
+ .desc(
+ `
+ Test that using a resolve target with that mipmap level count is greater than 1 is invalid for
+ color attachments.
+ `
+ )
+ .fn(async t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ mipLevelCount: 2 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,usage')
+ .desc(
+ `
+ Test that using a resolve target whose usage is not RENDER_ATTACHMENT is invalid for color
+ attachments.
+ `
+ )
+ .paramsSimple([
+ { usage: GPUConst.TextureUsage.COPY_SRC | GPUConst.TextureUsage.COPY_DST },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.TEXTURE_BINDING },
+ { usage: GPUConst.TextureUsage.STORAGE_BINDING | GPUConst.TextureUsage.STORAGE },
+ { usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.TEXTURE_BINDING },
+ ])
+ .fn(async t => {
+ const { usage } = t.params;
+
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ usage });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ const isValid = usage & GPUConst.TextureUsage.RENDER_ATTACHMENT ? true : false;
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('resolveTarget,error_state')
+ .desc(`Test that a resolve target that has a error is invalid for color attachments.`)
+ .fn(async t => {
+ const ARRAY_LAYER_COUNT = 1;
+
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ arrayLayerCount: ARRAY_LAYER_COUNT });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ t.expectValidationError(() => {
+ colorAttachment.resolveTarget = resolveTargetTexture.createView({
+ dimension: '2d',
+ format: 'rgba8unorm',
+ baseArrayLayer: ARRAY_LAYER_COUNT + 1,
+ });
+ });
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,single_sample_count')
+ .desc(
+ `
+ Test that a resolve target that has multi sample color attachment and a single resolve target is
+ valid.
+ `
+ )
+ .fn(async t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ sampleCount: 1 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ });
+
+g.test('resolveTarget,different_format')
+ .desc(`Test that a resolve target that has a different format is invalid.`)
+ .fn(async t => {
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({ format: 'bgra8unorm' });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTexture.createView();
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ });
+
+g.test('resolveTarget,different_size')
+ .desc(
+ `
+ Test that a resolve target that has a different size with the color attachment is invalid.
+ `
+ )
+ .fn(async t => {
+ const size = 16;
+ const multisampledColorTexture = t.createTexture({ width: size, height: size, sampleCount: 4 });
+ const resolveTargetTexture = t.createTexture({
+ width: size * 2,
+ height: size * 2,
+ mipLevelCount: 2,
+ });
+
+ {
+ const resolveTargetTextureView = resolveTargetTexture.createView({
+ baseMipLevel: 0,
+ mipLevelCount: 1,
+ });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTextureView;
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ const resolveTargetTextureView = resolveTargetTexture.createView({ baseMipLevel: 1 });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTargetTextureView;
+
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [colorAttachment],
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ });
+
+g.test('depth_stencil_attachment,sample_counts_mismatch')
+ .desc(
+ `
+ Test that the depth stencil attachment that has different number of samples with the color
+ attachment is invalid.
+ `
+ )
+ .fn(async t => {
+ const multisampledDepthStencilTexture = t.createTexture({
+ sampleCount: 4,
+ format: 'depth24plus-stencil8',
+ });
+
+ {
+ // It is not allowed to use a depth stencil attachment whose sample count is different from
+ // the one of the color attachment.
+ const depthStencilTexture = t.createTexture({
+ sampleCount: 1,
+ format: 'depth24plus-stencil8',
+ });
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(depthStencilTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ const colorTexture = t.createTexture({ sampleCount: 1 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(false, descriptor);
+ }
+ {
+ // It is allowed to use a multisampled depth stencil attachment whose sample count is equal to
+ // the one of the color attachment.
+ const multisampledColorTexture = t.createTexture({ sampleCount: 4 });
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [t.getColorAttachment(multisampledColorTexture)],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ {
+ // It is allowed to use a multisampled depth stencil attachment with no color attachment.
+ const descriptor: GPURenderPassDescriptor = {
+ colorAttachments: [],
+ depthStencilAttachment: t.getDepthStencilAttachment(multisampledDepthStencilTexture),
+ };
+
+ t.tryRenderPass(true, descriptor);
+ }
+ });
+
+g.test('depth_stencil_attachment')
+ .desc(
+ `
+ Test GPURenderPassDepthStencilAttachment Usage:
+ - depthReadOnly and stencilReadOnly must match if the format is a combined depth-stencil format.
+ - depthLoadOp and depthStoreOp must be provided iff the format has a depth aspect and
+ depthReadOnly is not true.
+ - stencilLoadOp and stencilStoreOp must be provided iff the format has a stencil aspect and
+ stencilReadOnly is not true.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases()
+ .combine('depthReadOnly', [false, true])
+ .combine('stencilReadOnly', [false, true])
+ .combine('setDepthLoadStoreOp', [false, true])
+ .combine('setStencilLoadStoreOp', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const {
+ format,
+ depthReadOnly,
+ stencilReadOnly,
+ setDepthLoadStoreOp,
+ setStencilLoadStoreOp,
+ } = t.params;
+
+ let isValid = true;
+ const info = kTextureFormatInfo[format];
+ if (info.depth && info.stencil) {
+ isValid &&= depthReadOnly === stencilReadOnly;
+ }
+
+ if (info.depth && !depthReadOnly) {
+ isValid &&= setDepthLoadStoreOp;
+ } else {
+ isValid &&= !setDepthLoadStoreOp;
+ }
+
+ if (info.stencil && !stencilReadOnly) {
+ isValid &&= setStencilLoadStoreOp;
+ } else {
+ isValid &&= !setStencilLoadStoreOp;
+ }
+
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: t.createTexture({ format }).createView(),
+ depthReadOnly,
+ stencilReadOnly,
+ };
+
+ if (setDepthLoadStoreOp) {
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'store';
+ }
+ if (setStencilLoadStoreOp) {
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'store';
+ }
+
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(t.createTexture())],
+ depthStencilAttachment,
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('depth_stencil_attachment,depth_clear_value')
+ .desc(
+ `
+ Test that depthClearValue is invalid if the value is out of the range(0.0 and 1.0) only when
+ depthLoadOp is 'clear'.
+ `
+ )
+ .params(u =>
+ u
+ .combine('depthLoadOp', ['load', 'clear', undefined] as const)
+ .combineWithParams([
+ { depthClearValue: -1.0 },
+ { depthClearValue: 0.0 },
+ { depthClearValue: 0.5 },
+ { depthClearValue: 1.0 },
+ { depthClearValue: 1.5 },
+ ])
+ )
+ .fn(t => {
+ const { depthLoadOp, depthClearValue } = t.params;
+
+ const depthStencilTexture = t.createTexture({
+ format: depthLoadOp === undefined ? 'stencil8' : 'depth24plus-stencil8',
+ });
+ const depthStencilAttachment = t.getDepthStencilAttachment(depthStencilTexture);
+ depthStencilAttachment.depthClearValue = depthClearValue;
+ depthStencilAttachment.depthLoadOp = depthLoadOp;
+ if (depthLoadOp === undefined) {
+ depthStencilAttachment.depthStoreOp = undefined;
+ }
+
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(t.createTexture())],
+ depthStencilAttachment,
+ };
+
+ const isValid = !(depthLoadOp === 'clear' && (depthClearValue < 0.0 || depthClearValue > 1.0));
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('resolveTarget,format_supports_resolve')
+ .desc(
+ `
+ For all formats that support 'multisample', test that they can be used as a resolveTarget
+ if and only if they support 'resolve'.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .filter(t => kTextureFormatInfo[t.format].multisample)
+ )
+ .fn(async t => {
+ const { format } = t.params;
+ const multisampledColorTexture = t.createTexture({ format, sampleCount: 4 });
+ const resolveTarget = t.createTexture({ format });
+
+ const colorAttachment = t.getColorAttachment(multisampledColorTexture);
+ colorAttachment.resolveTarget = resolveTarget.createView();
+
+ t.tryRenderPass(kTextureFormatInfo[format].resolve, {
+ colorAttachments: [colorAttachment],
+ });
+ });
+
+g.test('timestampWrites,query_set_type')
+ .desc(
+ `
+ Test that all entries of the timestampWrites must have type 'timestamp'. If all query types are
+ not 'timestamp', a validation error should be generated.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('queryTypeA', kQueryTypes)
+ .combine('queryTypeB', kQueryTypes)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { queryTypeA, queryTypeB } = t.params;
+
+ const timestampWriteA = {
+ querySet: t.device.createQuerySet({ type: queryTypeA, count: 1 }),
+ queryIndex: 0,
+ location: 'beginning' as const,
+ };
+
+ const timestampWriteB = {
+ querySet: t.device.createQuerySet({ type: queryTypeB, count: 1 }),
+ queryIndex: 0,
+ location: 'end' as const,
+ };
+
+ const isValid = queryTypeA === 'timestamp' && queryTypeB === 'timestamp';
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites: [timestampWriteA, timestampWriteB],
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('timestamp_writes_location')
+ .desc('Test that entries in timestampWrites do not have the same location.')
+ .params(u =>
+ u //
+ .combine('locationA', ['beginning', 'end'] as const)
+ .combine('locationB', ['beginning', 'end'] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { locationA, locationB } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: 'timestamp',
+ count: 2,
+ });
+
+ const timestampWriteA = {
+ querySet,
+ queryIndex: 0,
+ location: locationA,
+ };
+
+ const timestampWriteB = {
+ querySet,
+ queryIndex: 1,
+ location: locationB,
+ };
+
+ const isValid = locationA !== locationB;
+
+ const colorTexture = t.createTexture({ format: 'rgba8unorm' });
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites: [timestampWriteA, timestampWriteB],
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('timestampWrite,query_index')
+ .desc(`Test that querySet.count should be greater than timestampWrite.queryIndex.`)
+ .params(u => u.combine('queryIndex', [0, 1, 2, 3]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { queryIndex } = t.params;
+
+ const querySetCount = 2;
+
+ const timestampWrite = {
+ querySet: t.device.createQuerySet({ type: 'timestamp', count: querySetCount }),
+ queryIndex,
+ location: 'beginning' as const,
+ };
+
+ const isValid = queryIndex < querySetCount;
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites: [timestampWrite],
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('timestampWrite,same_query_index')
+ .desc(
+ `
+ Test that timestampWrites is invalid if each entry has the same queryIndex in the same querySet.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('queryIndexA', [0, 1])
+ .combine('queryIndexB', [0, 1])
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ })
+ .fn(async t => {
+ const { queryIndexA, queryIndexB } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: 'timestamp',
+ count: 2,
+ });
+
+ const timestampWriteA = {
+ querySet,
+ queryIndex: queryIndexA,
+ location: 'beginning' as const,
+ };
+
+ const timestampWriteB = {
+ querySet,
+ queryIndex: queryIndexB,
+ location: 'end' as const,
+ };
+
+ const isValid = queryIndexA !== queryIndexB;
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ timestampWrites: [timestampWriteA, timestampWriteB],
+ };
+
+ t.tryRenderPass(isValid, descriptor);
+ });
+
+g.test('occlusionQuerySet,query_set_type')
+ .desc(`Test that occlusionQuerySet must have type 'occlusion'.`)
+ .params(u => u.combine('queryType', kQueryTypes))
+ .beforeAllSubcases(t => {
+ if (t.params.queryType === 'timestamp') {
+ t.selectDeviceOrSkipTestCase(['timestamp-query']);
+ }
+ })
+ .fn(async t => {
+ const { queryType } = t.params;
+
+ const querySet = t.device.createQuerySet({
+ type: queryType,
+ count: 1,
+ });
+
+ const colorTexture = t.createTexture();
+ const descriptor = {
+ colorAttachments: [t.getColorAttachment(colorTexture)],
+ occlusionQuerySet: querySet,
+ };
+
+ const isValid = queryType === 'occlusion';
+ t.tryRenderPass(isValid, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts
new file mode 100644
index 0000000000..c9a34c3157
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/resolve.spec.ts
@@ -0,0 +1,192 @@
+export const description = `
+Validation tests for render pass resolve.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+const kNumColorAttachments = 4;
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('resolve_attachment')
+ .desc(
+ `
+Test various validation behaviors when a resolveTarget is provided.
+
+- base case (valid).
+- resolve source is not multisampled.
+- resolve target is not single sampled.
+- resolve target missing RENDER_ATTACHMENT usage.
+- resolve target must have exactly one subresource:
+ - base mip level {0, >0}, mip level count {1, >1}.
+ - base array layer {0, >0}, array layer count {1, >1}.
+- resolve target GPUTextureView is invalid
+- resolve source and target have different formats.
+ - rgba8unorm -> {bgra8unorm, rgba8unorm-srgb}
+ - {bgra8unorm, rgba8unorm-srgb} -> rgba8unorm
+ - test with other color attachments having a different format
+- resolve source and target have different sizes.
+`
+ )
+ .paramsSimple([
+ // control case should be valid
+ { _valid: true },
+ // a single sampled resolve source should cause a validation error.
+ { colorAttachmentSamples: 1, _valid: false },
+ // a multisampled resolve target should cause a validation error.
+ { resolveTargetSamples: 4, _valid: false },
+ // resolveTargetUsage without RENDER_ATTACHMENT usage should cause a validation error.
+ { resolveTargetUsage: GPUConst.TextureUsage.COPY_SRC, _valid: false },
+ // non-zero resolve target base mip level should be valid.
+ {
+ resolveTargetViewBaseMipLevel: 1,
+ resolveTargetHeight: 4,
+ resolveTargetWidth: 4,
+ _valid: true,
+ },
+ // a validation error should be created when resolveTarget is invalid.
+ { resolveTargetInvalid: true, _valid: false },
+ // a validation error should be created when mip count > 1
+ { resolveTargetViewMipCount: 2, _valid: false },
+ {
+ resolveTargetViewBaseMipLevel: 1,
+ resolveTargetViewMipCount: 2,
+ resolveTargetHeight: 4,
+ resolveTargetWidth: 4,
+ _valid: false,
+ },
+ // non-zero resolve target base array layer should be valid.
+ { resolveTargetViewBaseArrayLayer: 1, _valid: true },
+ // a validation error should be created when array layer count > 1
+ { resolveTargetViewArrayLayerCount: 2, _valid: false },
+ { resolveTargetViewBaseArrayLayer: 1, resolveTargetViewArrayLayerCount: 2, _valid: false },
+ // other color attachments resolving with a different format should be valid.
+ { otherAttachmentFormat: 'bgra8unorm', _valid: true },
+ // mismatched colorAttachment and resolveTarget formats should cause a validation error.
+ { colorAttachmentFormat: 'bgra8unorm', _valid: false },
+ { colorAttachmentFormat: 'rgba8unorm-srgb', _valid: false },
+ { resolveTargetFormat: 'bgra8unorm', _valid: false },
+ { resolveTargetFormat: 'rgba8unorm-srgb', _valid: false },
+ // mismatched colorAttachment and resolveTarget sizes should cause a validation error.
+ { colorAttachmentHeight: 4, _valid: false },
+ { colorAttachmentWidth: 4, _valid: false },
+ { resolveTargetHeight: 4, _valid: false },
+ { resolveTargetWidth: 4, _valid: false },
+ ] as const)
+ .fn(async t => {
+ const {
+ colorAttachmentFormat = 'rgba8unorm',
+ resolveTargetFormat = 'rgba8unorm',
+ otherAttachmentFormat = 'rgba8unorm',
+ colorAttachmentSamples = 4,
+ resolveTargetSamples = 1,
+ resolveTargetUsage = GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ resolveTargetInvalid = false,
+ resolveTargetViewMipCount = 1,
+ resolveTargetViewBaseMipLevel = 0,
+ resolveTargetViewArrayLayerCount = 1,
+ resolveTargetViewBaseArrayLayer = 0,
+ colorAttachmentHeight = 2,
+ colorAttachmentWidth = 2,
+ resolveTargetHeight = 2,
+ resolveTargetWidth = 2,
+ _valid,
+ } = t.params;
+
+ // Run the test in a nested loop such that the configured color attachment with resolve target
+ // is tested while occupying each individual colorAttachment slot.
+ for (let resolveSlot = 0; resolveSlot < kNumColorAttachments; resolveSlot++) {
+ const renderPassColorAttachmentDescriptors: GPURenderPassColorAttachment[] = [];
+ for (
+ let colorAttachmentSlot = 0;
+ colorAttachmentSlot < kNumColorAttachments;
+ colorAttachmentSlot++
+ ) {
+ // resolveSlot === colorAttachmentSlot denotes the color attachment slot that contains the
+ // color attachment with resolve target.
+ if (resolveSlot === colorAttachmentSlot) {
+ // Create the color attachment with resolve target with the configurable parameters.
+ const resolveSourceColorAttachment = t.device.createTexture({
+ format: colorAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: colorAttachmentSamples,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const resolveTarget = t.device.createTexture({
+ format: resolveTargetFormat,
+ size: {
+ width: resolveTargetWidth,
+ height: resolveTargetHeight,
+ depthOrArrayLayers:
+ resolveTargetViewBaseArrayLayer + resolveTargetViewArrayLayerCount,
+ },
+ sampleCount: resolveTargetSamples,
+ mipLevelCount: resolveTargetViewBaseMipLevel + resolveTargetViewMipCount,
+ usage: resolveTargetUsage,
+ });
+
+ renderPassColorAttachmentDescriptors.push({
+ view: resolveSourceColorAttachment.createView(),
+ loadOp: 'load',
+ storeOp: 'discard',
+ resolveTarget: resolveTargetInvalid
+ ? t.getErrorTextureView()
+ : resolveTarget.createView({
+ dimension: resolveTargetViewArrayLayerCount === 1 ? '2d' : '2d-array',
+ mipLevelCount: resolveTargetViewMipCount,
+ arrayLayerCount: resolveTargetViewArrayLayerCount,
+ baseMipLevel: resolveTargetViewBaseMipLevel,
+ baseArrayLayer: resolveTargetViewBaseArrayLayer,
+ }),
+ });
+ } else {
+ // Create a basic texture to fill other color attachment slots. This texture's dimensions
+ // and sample count must match the resolve source color attachment to be valid.
+ const colorAttachment = t.device.createTexture({
+ format: otherAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: colorAttachmentSamples,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const resolveTarget = t.device.createTexture({
+ format: otherAttachmentFormat,
+ size: {
+ width: colorAttachmentWidth,
+ height: colorAttachmentHeight,
+ depthOrArrayLayers: 1,
+ },
+ sampleCount: 1,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ renderPassColorAttachmentDescriptors.push({
+ view: colorAttachment.createView(),
+ loadOp: 'load',
+ storeOp: 'discard',
+ resolveTarget: resolveTarget.createView(),
+ });
+ }
+ }
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: renderPassColorAttachmentDescriptors,
+ });
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !_valid);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/storeOp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/storeOp.spec.ts
new file mode 100644
index 0000000000..8aa63742a9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pass/storeOp.spec.ts
@@ -0,0 +1,75 @@
+export const description = `
+API Validation Tests for RenderPass StoreOp.
+
+Test Coverage:
+ - Tests that when depthReadOnly is true, depthStoreOp must be 'store'.
+ - When depthReadOnly is true and depthStoreOp is 'discard', an error should be generated.
+
+ - Tests that when stencilReadOnly is true, stencilStoreOp must be 'store'.
+ - When stencilReadOnly is true and stencilStoreOp is 'discard', an error should be generated.
+
+ - Tests that the depthReadOnly value matches the stencilReadOnly value.
+ - When depthReadOnly does not match stencilReadOnly, an error should be generated.
+
+ - Tests that depthReadOnly and stencilReadOnly default to false.
+
+TODO: test interactions with depthClearValue too
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('store_op_and_read_only')
+ .paramsSimple([
+ { readonly: true, _valid: true },
+ // Using depthReadOnly=true and depthStoreOp='discard' should cause a validation error.
+ { readonly: true, depthStoreOp: 'discard', _valid: false },
+ // Using stencilReadOnly=true and stencilStoreOp='discard' should cause a validation error.
+ { readonly: true, stencilStoreOp: 'discard', _valid: false },
+ // Mismatched depthReadOnly and stencilReadOnly values should cause a validation error.
+ { readonly: false, _valid: true },
+ { readonly: false, depthReadOnly: true, _valid: false },
+ { readonly: false, stencilReadOnly: true, _valid: false },
+ // depthReadOnly and stencilReadOnly should default to false.
+ { readonly: undefined, _valid: true },
+ { readonly: undefined, depthReadOnly: true, _valid: false },
+ { readonly: undefined, stencilReadOnly: true, _valid: false },
+ ] as const)
+ .fn(async t => {
+ const {
+ readonly,
+ depthStoreOp = 'store',
+ depthReadOnly = readonly,
+ stencilStoreOp = 'store',
+ stencilReadOnly = readonly,
+ _valid,
+ } = t.params;
+
+ const depthAttachment = t.device.createTexture({
+ format: 'depth24plus-stencil8',
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const depthAttachmentView = depthAttachment.createView();
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: depthAttachmentView,
+ depthLoadOp: 'load',
+ depthStoreOp,
+ depthReadOnly,
+ stencilLoadOp: 'load',
+ stencilStoreOp,
+ stencilReadOnly,
+ },
+ });
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !_valid);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts
new file mode 100644
index 0000000000..eb4cfa00a5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/common.ts
@@ -0,0 +1,68 @@
+import { kTextureFormatInfo } from '../../../capability_info.js';
+import {
+ getFragmentShaderCodeWithOutput,
+ getPlainTypeInfo,
+ kDefaultVertexShaderCode,
+} from '../../../util/shader.js';
+import { ValidationTest } from '../validation_test.js';
+
+const values = [0, 1, 0, 1];
+export class CreateRenderPipelineValidationTest extends ValidationTest {
+ getDescriptor(
+ options: {
+ primitive?: GPUPrimitiveState;
+ targets?: GPUColorTargetState[];
+ multisample?: GPUMultisampleState;
+ depthStencil?: GPUDepthStencilState;
+ fragmentShaderCode?: string;
+ noFragment?: boolean;
+ fragmentConstants?: Record<string, GPUPipelineConstantValue>;
+ } = {}
+ ): GPURenderPipelineDescriptor {
+ const defaultTargets: GPUColorTargetState[] = [{ format: 'rgba8unorm' }];
+ const {
+ primitive = {},
+ targets = defaultTargets,
+ multisample = {},
+ depthStencil,
+ fragmentShaderCode = getFragmentShaderCodeWithOutput([
+ {
+ values,
+ plainType: getPlainTypeInfo(
+ kTextureFormatInfo[targets[0] ? targets[0].format : 'rgba8unorm'].sampleType
+ ),
+ componentCount: 4,
+ },
+ ]),
+ noFragment = false,
+ fragmentConstants = {},
+ } = options;
+
+ return {
+ vertex: {
+ module: this.device.createShaderModule({
+ code: kDefaultVertexShaderCode,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: noFragment
+ ? undefined
+ : {
+ module: this.device.createShaderModule({
+ code: fragmentShaderCode,
+ }),
+ entryPoint: 'main',
+ targets,
+ constants: fragmentConstants,
+ },
+ layout: this.getPipelineLayout(),
+ primitive,
+ multisample,
+ depthStencil,
+ };
+ }
+
+ getPipelineLayout(): GPUPipelineLayout {
+ return this.device.createPipelineLayout({ bindGroupLayouts: [] });
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts
new file mode 100644
index 0000000000..e242ed8114
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/depth_stencil_state.spec.ts
@@ -0,0 +1,203 @@
+export const description = `
+This test dedicatedly tests validation of GPUDepthStencilState of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../common/util/util.js';
+import {
+ kTextureFormats,
+ kTextureFormatInfo,
+ kDepthStencilFormats,
+ kCompareFunctions,
+ kStencilOperations,
+} from '../../../capability_info.js';
+import { getFragmentShaderCodeWithOutput } from '../../../util/shader.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+g.test('format')
+ .desc(`The texture format in depthStencilState must be a depth/stencil format.`)
+ .params(u => u.combine('isAsync', [false, true]).combine('format', kTextureFormats))
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({ depthStencil: { format } });
+
+ t.doCreateRenderPipelineTest(isAsync, info.depth || info.stencil, descriptor);
+ });
+
+g.test('depth_test')
+ .desc(
+ `Depth aspect must be contained in the format if depth test is enabled in depthStencilState.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', kDepthStencilFormats)
+ .combine('depthCompare', [undefined, ...kCompareFunctions])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format, depthCompare } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({
+ depthStencil: { format, depthCompare },
+ });
+
+ const depthTestEnabled = depthCompare !== undefined && depthCompare !== 'always';
+ t.doCreateRenderPipelineTest(isAsync, !depthTestEnabled || info.depth, descriptor);
+ });
+
+g.test('depth_write')
+ .desc(
+ `Depth aspect must be contained in the format if depth write is enabled in depthStencilState.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', kDepthStencilFormats)
+ .combine('depthWriteEnabled', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format, depthWriteEnabled } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({
+ depthStencil: { format, depthWriteEnabled },
+ });
+ t.doCreateRenderPipelineTest(isAsync, !depthWriteEnabled || info.depth, descriptor);
+ });
+
+g.test('depth_write,frag_depth')
+ .desc(`Depth aspect must be contained in the format if frag_depth is written in fragment stage.`)
+ .params(u =>
+ u.combine('isAsync', [false, true]).combine('format', [undefined, ...kDepthStencilFormats])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ if (format !== undefined) {
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ }
+ })
+ .fn(async t => {
+ const { isAsync, format } = t.params;
+
+ const descriptor = t.getDescriptor({
+ // Keep one color target so that the pipeline is still valid with no depth stencil target.
+ targets: [{ format: 'rgba8unorm' }],
+ depthStencil: format ? { format, depthWriteEnabled: true } : undefined,
+ fragmentShaderCode: getFragmentShaderCodeWithOutput(
+ [{ values: [1, 1, 1, 1], plainType: 'f32', componentCount: 4 }],
+ { value: 0.5 }
+ ),
+ });
+
+ const hasDepth = format ? kTextureFormatInfo[format].depth : false;
+ t.doCreateRenderPipelineTest(isAsync, hasDepth, descriptor);
+ });
+
+g.test('stencil_test')
+ .desc(
+ `Stencil aspect must be contained in the format if stencil test is enabled in depthStencilState.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', kDepthStencilFormats)
+ .combine('face', ['front', 'back'] as const)
+ .combine('compare', [undefined, ...kCompareFunctions])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format, face, compare } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ let descriptor: GPURenderPipelineDescriptor;
+ if (face === 'front') {
+ descriptor = t.getDescriptor({ depthStencil: { format, stencilFront: { compare } } });
+ } else {
+ descriptor = t.getDescriptor({ depthStencil: { format, stencilBack: { compare } } });
+ }
+
+ const stencilTestEnabled = compare !== undefined && compare !== 'always';
+ t.doCreateRenderPipelineTest(isAsync, !stencilTestEnabled || info.stencil, descriptor);
+ });
+
+g.test('stencil_write')
+ .desc(
+ `Stencil aspect must be contained in the format if stencil write is enabled in depthStencilState.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', kDepthStencilFormats)
+ .combine('faceAndOpType', [
+ 'frontFailOp',
+ 'frontDepthFailOp',
+ 'frontPassOp',
+ 'backFailOp',
+ 'backDepthFailOp',
+ 'backPassOp',
+ ] as const)
+ .combine('op', [undefined, ...kStencilOperations])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format, faceAndOpType, op } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ let depthStencil: GPUDepthStencilState;
+ switch (faceAndOpType) {
+ case 'frontFailOp':
+ depthStencil = { format, stencilFront: { failOp: op } };
+ break;
+ case 'frontDepthFailOp':
+ depthStencil = { format, stencilFront: { depthFailOp: op } };
+ break;
+ case 'frontPassOp':
+ depthStencil = { format, stencilFront: { passOp: op } };
+ break;
+ case 'backFailOp':
+ depthStencil = { format, stencilBack: { failOp: op } };
+ break;
+ case 'backDepthFailOp':
+ depthStencil = { format, stencilBack: { depthFailOp: op } };
+ break;
+ case 'backPassOp':
+ depthStencil = { format, stencilBack: { passOp: op } };
+ break;
+ default:
+ unreachable();
+ }
+ const descriptor = t.getDescriptor({ depthStencil });
+
+ const stencilWriteEnabled = op !== undefined && op !== 'keep';
+ t.doCreateRenderPipelineTest(isAsync, !stencilWriteEnabled || info.stencil, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts
new file mode 100644
index 0000000000..ffedddea2c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/fragment_state.spec.ts
@@ -0,0 +1,392 @@
+export const description = `
+This test dedicatedly tests validation of GPUFragmentState of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { range } from '../../../../common/util/util.js';
+import {
+ kTextureFormats,
+ kRenderableColorTextureFormats,
+ kTextureFormatInfo,
+ kBlendFactors,
+ kBlendOperations,
+ kMaxColorAttachments,
+} from '../../../capability_info.js';
+import {
+ getFragmentShaderCodeWithOutput,
+ getPlainTypeInfo,
+ kDefaultFragmentShaderCode,
+} from '../../../util/shader.js';
+import { kTexelRepresentationInfo } from '../../../util/texture/texel_data.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+const values = [0, 1, 0, 1];
+
+g.test('color_target_exists')
+ .desc(`Tests creating a complete render pipeline requires at least one color target state.`)
+ .params(u => u.combine('isAsync', [false, true]))
+ .fn(async t => {
+ const { isAsync } = t.params;
+
+ const goodDescriptor = t.getDescriptor({
+ targets: [{ format: 'rgba8unorm' }],
+ });
+
+ // Control case
+ t.doCreateRenderPipelineTest(isAsync, true, goodDescriptor);
+
+ // Fail because lack of color states
+ const badDescriptor = t.getDescriptor({
+ targets: [],
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, false, badDescriptor);
+ });
+
+g.test('targets_format_renderable')
+ .desc(`Tests that color target state format must have RENDER_ATTACHMENT capability.`)
+ .params(u => u.combine('isAsync', [false, true]).combine('format', kTextureFormats))
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({ targets: [{ format }] });
+
+ t.doCreateRenderPipelineTest(isAsync, info.renderable && info.color, descriptor);
+ });
+
+g.test('limits,maxColorAttachments')
+ .desc(
+ `Tests that color state targets length must not be larger than device.limits.maxColorAttachments.`
+ )
+ .params(u => u.combine('isAsync', [false, true]).combine('targetsLength', [8, 9]))
+ .fn(async t => {
+ const { isAsync, targetsLength } = t.params;
+
+ const descriptor = t.getDescriptor({
+ targets: range(targetsLength, i => {
+ // Set writeMask to 0 for attachments without fragment output
+ return { format: 'rg8unorm', writeMask: i === 0 ? 0xf : 0 };
+ }),
+ fragmentShaderCode: kDefaultFragmentShaderCode,
+ });
+
+ t.doCreateRenderPipelineTest(
+ isAsync,
+ targetsLength <= t.device.limits.maxColorAttachments,
+ descriptor
+ );
+ });
+
+g.test('limits,maxColorAttachmentBytesPerSample,aligned')
+ .desc(
+ `
+ Tests that the total color attachment bytes per sample must not be larger than
+ maxColorAttachmentBytesPerSample when using the same format for multiple attachments.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine(
+ 'attachmentCount',
+ range(kMaxColorAttachments, i => i + 1)
+ )
+ .combine('isAsync', [false, true])
+ )
+ .fn(async t => {
+ const { format, attachmentCount, isAsync } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({
+ targets: range(attachmentCount, () => {
+ return { format, writeMask: 0 };
+ }),
+ });
+ const shouldError =
+ info.renderTargetPixelByteCost === undefined ||
+ info.renderTargetPixelByteCost * attachmentCount >
+ t.device.limits.maxColorAttachmentBytesPerSample;
+
+ t.doCreateRenderPipelineTest(isAsync, !shouldError, descriptor);
+ });
+
+g.test('limits,maxColorAttachmentBytesPerSample,unaligned')
+ .desc(
+ `
+ Tests that the total color attachment bytes per sample must not be larger than
+ maxColorAttachmentBytesPerSample when using various sets of (potentially) unaligned formats.
+ `
+ )
+ .params(u =>
+ u
+ .combineWithParams([
+ // Alignment causes the first 1 byte R8Unorm to become 4 bytes. So even though
+ // 1+4+8+16+1 < 32, the 4 byte alignment requirement of R32Float makes the first R8Unorm
+ // become 4 and 4+4+8+16+1 > 32. Re-ordering this so the R8Unorm's are at the end, however
+ // is allowed: 4+8+16+1+1 < 32.
+ {
+ formats: [
+ 'r8unorm',
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _success: true,
+ },
+ {
+ formats: [
+ 'r32float',
+ 'rgba8unorm',
+ 'rgba32float',
+ 'r8unorm',
+ 'r8unorm',
+ ] as GPUTextureFormat[],
+ _success: false,
+ },
+ ])
+ .beginSubcases()
+ .combine('isAsync', [false, true])
+ )
+ .fn(async t => {
+ const { formats, _success, isAsync } = t.params;
+
+ const descriptor = t.getDescriptor({
+ targets: formats.map(f => {
+ return { format: f, writeMask: 0 };
+ }),
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('targets_format_filterable')
+ .desc(`Tests that color target state format must be filterable if blend is not undefined.`)
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine('hasBlend', [false, true])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const { isAsync, format, hasBlend } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({
+ targets: [
+ {
+ format,
+ blend: hasBlend ? { color: {}, alpha: {} } : undefined,
+ },
+ ],
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, !hasBlend || info.sampleType === 'float', descriptor);
+ });
+
+g.test('targets_blend')
+ .desc(
+ `
+ For the blend components on either GPUBlendState.color or GPUBlendState.alpha:
+ - Tests if the combination of 'srcFactor', 'dstFactor' and 'operation' is valid (if the blend
+ operation is "min" or "max", srcFactor and dstFactor must be "one").
+ `
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('component', ['color', 'alpha'] as const)
+ .beginSubcases()
+ .combine('srcFactor', kBlendFactors)
+ .combine('dstFactor', kBlendFactors)
+ .combine('operation', kBlendOperations)
+ )
+ .fn(async t => {
+ const { isAsync, component, srcFactor, dstFactor, operation } = t.params;
+
+ const defaultBlendComponent: GPUBlendComponent = {
+ srcFactor: 'src-alpha',
+ dstFactor: 'dst-alpha',
+ operation: 'add',
+ };
+ const blendComponentToTest: GPUBlendComponent = {
+ srcFactor,
+ dstFactor,
+ operation,
+ };
+ const format = 'rgba8unorm';
+
+ const descriptor = t.getDescriptor({
+ targets: [
+ {
+ format,
+ blend: {
+ color: component === 'color' ? blendComponentToTest : defaultBlendComponent,
+ alpha: component === 'alpha' ? blendComponentToTest : defaultBlendComponent,
+ },
+ },
+ ],
+ });
+
+ if (operation === 'min' || operation === 'max') {
+ const _success = srcFactor === 'one' && dstFactor === 'one';
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ } else {
+ t.doCreateRenderPipelineTest(isAsync, true, descriptor);
+ }
+ });
+
+g.test('targets_write_mask')
+ .desc(`Tests that color target state write mask must be < 16.`)
+ .params(u => u.combine('isAsync', [false, true]).combine('writeMask', [0, 0xf, 0x10, 0x80000001]))
+ .fn(async t => {
+ const { isAsync, writeMask } = t.params;
+
+ const descriptor = t.getDescriptor({
+ targets: [
+ {
+ format: 'rgba8unorm',
+ writeMask,
+ },
+ ],
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, writeMask < 16, descriptor);
+ });
+
+g.test('pipeline_output_targets')
+ .desc(
+ `Pipeline fragment output types must be compatible with target color state format
+ - The scalar type (f32, i32, or u32) must match the sample type of the format.
+ - The componentCount of the fragment output (e.g. f32, vec2, vec3, vec4) must not have fewer
+ channels than that of the color attachment texture formats. Extra components are allowed and are discarded.
+
+ Otherwise, color state write mask must be 0.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', [undefined, ...kRenderableColorTextureFormats] as const)
+ .beginSubcases()
+ .combine('shaderOutput', [
+ undefined,
+ ...u.combine('scalar', ['f32', 'u32', 'i32'] as const).combine('count', [1, 2, 3, 4]),
+ ])
+ // We only care about testing writeMask if there is an attachment but no shader output.
+ .expand('writeMask', p =>
+ p.format !== undefined && p.shaderOutput !== undefined ? [0, 0x1, 0x2, 0x4, 0x8] : [0xf]
+ )
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { isAsync, format, writeMask, shaderOutput } = t.params;
+
+ const descriptor = t.getDescriptor({
+ targets: format ? [{ format, writeMask }] : [],
+ // To have a dummy depthStencil attachment to avoid having no attachment at all which is invalid
+ depthStencil: { format: 'depth24plus' },
+ fragmentShaderCode: getFragmentShaderCodeWithOutput(
+ shaderOutput
+ ? [{ values, plainType: shaderOutput.scalar, componentCount: shaderOutput.count }]
+ : []
+ ),
+ });
+
+ let success = true;
+ if (format) {
+ // There is a color target
+ if (shaderOutput) {
+ // The shader outputs to the color target
+ const info = kTextureFormatInfo[format];
+ success =
+ shaderOutput.scalar === getPlainTypeInfo(info.sampleType) &&
+ shaderOutput.count >= kTexelRepresentationInfo[format].componentOrder.length;
+ } else {
+ // The shader does not output to the color target
+ success = writeMask === 0;
+ }
+ }
+
+ t.doCreateRenderPipelineTest(isAsync, success, descriptor);
+ });
+
+g.test('pipeline_output_targets,blend')
+ .desc(
+ `On top of requirements from pipeline_output_targets, when blending is enabled and alpha channel is read indicated by any blend factor, an extra requirement is added:
+ - fragment output must be vec4.
+ `
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('format', ['r8unorm', 'rg8unorm', 'rgba8unorm', 'bgra8unorm'] as const)
+ .combine('componentCount', [1, 2, 3, 4])
+ .beginSubcases()
+ // The default srcFactor and dstFactor are 'one' and 'zero'. Override just one at a time.
+ .combineWithParams([
+ ...u.combine('colorSrcFactor', kBlendFactors),
+ ...u.combine('colorDstFactor', kBlendFactors),
+ ...u.combine('alphaSrcFactor', kBlendFactors),
+ ...u.combine('alphaDstFactor', kBlendFactors),
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+ t.selectDeviceOrSkipTestCase(info.feature);
+ })
+ .fn(async t => {
+ const sampleType = 'float';
+ const {
+ isAsync,
+ format,
+ componentCount,
+ colorSrcFactor,
+ colorDstFactor,
+ alphaSrcFactor,
+ alphaDstFactor,
+ } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ const descriptor = t.getDescriptor({
+ targets: [
+ {
+ format,
+ blend: {
+ color: { srcFactor: colorSrcFactor, dstFactor: colorDstFactor },
+ alpha: { srcFactor: alphaSrcFactor, dstFactor: alphaDstFactor },
+ },
+ },
+ ],
+ fragmentShaderCode: getFragmentShaderCodeWithOutput([
+ { values, plainType: getPlainTypeInfo(sampleType), componentCount },
+ ]),
+ });
+
+ const colorBlendReadsSrcAlpha =
+ colorSrcFactor?.includes('src-alpha') || colorDstFactor?.includes('src-alpha');
+ const meetsExtraBlendingRequirement = !colorBlendReadsSrcAlpha || componentCount === 4;
+ const _success =
+ info.sampleType === sampleType &&
+ componentCount >= kTexelRepresentationInfo[format].componentOrder.length &&
+ meetsExtraBlendingRequirement;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts
new file mode 100644
index 0000000000..0271339a40
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/inter_stage.spec.ts
@@ -0,0 +1,324 @@
+export const description = `
+Interface matching between vertex and fragment shader validation for createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { assert, range } from '../../../../common/util/util.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+function getVarName(i: number) {
+ return `v${i}`;
+}
+
+class InterStageMatchingValidationTest extends CreateRenderPipelineValidationTest {
+ getVertexStateWithOutputs(outputs: string[]): GPUVertexState {
+ return {
+ module: this.device.createShaderModule({
+ code: `
+ struct A {
+ ${outputs.map((v, i) => v.replace('__', getVarName(i))).join(',\n')},
+ @builtin(position) pos: vec4<f32>,
+ }
+ @vertex fn main() -> A {
+ var vertexOut: A;
+ vertexOut.pos = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ return vertexOut;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ };
+ }
+
+ getFragmentStateWithInputs(
+ inputs: string[],
+ hasBuiltinPosition: boolean = false
+ ): GPUFragmentState {
+ return {
+ targets: [{ format: 'rgba8unorm' }],
+ module: this.device.createShaderModule({
+ code: `
+ struct B {
+ ${inputs.map((v, i) => v.replace('__', getVarName(i))).join(',\n')},
+ ${hasBuiltinPosition ? '@builtin(position) pos: vec4<f32>' : ''}
+ }
+ @fragment fn main(fragmentIn: B) -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ };
+ }
+
+ getDescriptorWithStates(
+ vertex: GPUVertexState,
+ fragment: GPUFragmentState
+ ): GPURenderPipelineDescriptor {
+ return {
+ layout: 'auto',
+ vertex,
+ fragment,
+ };
+ }
+}
+
+export const g = makeTestGroup(InterStageMatchingValidationTest);
+
+g.test('location,mismatch')
+ .desc(`Tests that missing declaration at the same location should fail validation.`)
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ { outputs: ['@location(0) __: f32'], inputs: ['@location(0) __: f32'], _success: true },
+ { outputs: ['@location(0) __: f32'], inputs: ['@location(1) __: f32'], _success: false },
+ { outputs: ['@location(1) __: f32'], inputs: ['@location(0) __: f32'], _success: false },
+ {
+ outputs: ['@location(0) __: f32', '@location(1) __: f32'],
+ inputs: ['@location(1) __: f32', '@location(0) __: f32'],
+ _success: true,
+ },
+ {
+ outputs: ['@location(1) __: f32', '@location(0) __: f32'],
+ inputs: ['@location(0) __: f32', '@location(1) __: f32'],
+ _success: true,
+ },
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, outputs, inputs, _success } = t.params;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs(outputs),
+ t.getFragmentStateWithInputs(inputs)
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('location,superset')
+ .desc(`TODO: implement after spec is settled: https://github.com/gpuweb/gpuweb/issues/2038`)
+ .unimplemented();
+
+g.test('location,subset')
+ .desc(`Tests that validation should fail when vertex output is a subset of fragment input.`)
+ .params(u => u.combine('isAsync', [false, true]))
+ .fn(async t => {
+ const { isAsync } = t.params;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs(['@location(0) vout0: f32']),
+ t.getFragmentStateWithInputs(['@location(0) fin0: f32', '@location(1) fin1: f32'])
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, false, descriptor);
+ });
+
+g.test('type')
+ .desc(
+ `Tests that validation should fail when type of vertex output and fragment input at the same location doesn't match.`
+ )
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ { output: 'f32', input: 'f32' },
+ { output: 'i32', input: 'f32' },
+ { output: 'u32', input: 'f32' },
+ { output: 'u32', input: 'i32' },
+ { output: 'i32', input: 'u32' },
+ { output: 'vec2<f32>', input: 'vec2<f32>' },
+ { output: 'vec3<f32>', input: 'vec2<f32>' },
+ { output: 'vec2<f32>', input: 'vec3<f32>' },
+ { output: 'vec2<f32>', input: 'f32' },
+ { output: 'f32', input: 'vec2<f32>' },
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, output, input } = t.params;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs([`@location(0) @interpolate(flat) vout0: ${output}`]),
+ t.getFragmentStateWithInputs([`@location(0) @interpolate(flat) fin0: ${input}`])
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, output === input, descriptor);
+ });
+
+g.test('interpolation_type')
+ .desc(
+ `Tests that validation should fail when interpolation type of vertex output and fragment input at the same location doesn't match.`
+ )
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ // default is @interpolate(perspective, center)
+ { output: '', input: '' },
+ { output: '', input: '@interpolate(perspective)', _success: true },
+ { output: '', input: '@interpolate(perspective, center)', _success: true },
+ { output: '@interpolate(perspective)', input: '', _success: true },
+ { output: '', input: '@interpolate(linear)' },
+ { output: '@interpolate(perspective)', input: '@interpolate(perspective)' },
+ { output: '@interpolate(linear)', input: '@interpolate(perspective)' },
+ { output: '@interpolate(flat)', input: '@interpolate(perspective)' },
+ { output: '@interpolate(linear)', input: '@interpolate(flat)' },
+ { output: '@interpolate(linear, center)', input: '@interpolate(linear, center)' },
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, output, input, _success } = t.params;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]),
+ t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`])
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, _success ?? output === input, descriptor);
+ });
+
+g.test('interpolation_sampling')
+ .desc(
+ `Tests that validation should fail when interpolation sampling of vertex output and fragment input at the same location doesn't match.`
+ )
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ // default is @interpolate(perspective, center)
+ { output: '@interpolate(perspective)', input: '@interpolate(perspective)' },
+ {
+ output: '@interpolate(perspective)',
+ input: '@interpolate(perspective, center)',
+ _success: true,
+ },
+ { output: '@interpolate(linear, center)', input: '@interpolate(linear)', _success: true },
+ { output: '@interpolate(flat)', input: '@interpolate(flat)' },
+ { output: '@interpolate(perspective)', input: '@interpolate(perspective, sample)' },
+ { output: '@interpolate(perspective, center)', input: '@interpolate(perspective, sample)' },
+ {
+ output: '@interpolate(perspective, center)',
+ input: '@interpolate(perspective, centroid)',
+ },
+ { output: '@interpolate(perspective, centroid)', input: '@interpolate(perspective)' },
+ ])
+ )
+ .fn(async t => {
+ const { isAsync, output, input, _success } = t.params;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]),
+ t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`])
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, _success ?? output === input, descriptor);
+ });
+
+g.test('max_shader_variable_location')
+ .desc(
+ `Tests that validation should fail when there is location of user-defined output/input variable >= device.limits.maxInterStageShaderVariables`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ // User defined variable location = maxInterStageShaderVariables + locationDelta
+ .combine('locationDelta', [0, -1, -2])
+ )
+ .fn(async t => {
+ const { isAsync, locationDelta } = t.params;
+ const maxInterStageShaderVariables = t.device.limits.maxInterStageShaderVariables;
+ const location = maxInterStageShaderVariables + locationDelta;
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs([`@location(${location}) vout0: f32`]),
+ t.getFragmentStateWithInputs([`@location(${location}) fin0: f32`])
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, location < maxInterStageShaderVariables, descriptor);
+ });
+
+g.test('max_components_count,output')
+ .desc(
+ `Tests that validation should fail when scalar components of all user-defined outputs > max vertex shader output components.`
+ )
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ // Number of user-defined output scalar components in test shader = device.limits.maxInterStageShaderComponents + numScalarDelta.
+ { numScalarDelta: 0, topology: 'triangle-list', _success: true },
+ { numScalarDelta: 1, topology: 'triangle-list', _success: false },
+ { numScalarDelta: 0, topology: 'point-list', _success: false },
+ { numScalarDelta: -1, topology: 'point-list', _success: true },
+ ] as const)
+ )
+ .fn(async t => {
+ const { isAsync, numScalarDelta, topology, _success } = t.params;
+
+ const numScalarComponents = t.device.limits.maxInterStageShaderComponents + numScalarDelta;
+
+ const numVec4 = Math.floor(numScalarComponents / 4);
+ const numTrailingScalars = numScalarComponents % 4;
+ const numUserDefinedInterStageVariables = numTrailingScalars > 0 ? numVec4 + 1 : numVec4;
+
+ assert(numUserDefinedInterStageVariables <= t.device.limits.maxInterStageShaderVariables);
+
+ const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`);
+ const inputs = range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`);
+
+ if (numTrailingScalars > 0) {
+ const typeString = numTrailingScalars === 1 ? 'f32' : `vec${numTrailingScalars}<f32>`;
+ outputs.push(`@location(${numVec4}) vout${numVec4}: ${typeString}`);
+ inputs.push(`@location(${numVec4}) fin${numVec4}: ${typeString}`);
+ }
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs(outputs),
+ t.getFragmentStateWithInputs(inputs)
+ );
+ descriptor.primitive = { topology };
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('max_components_count,input')
+ .desc(
+ `Tests that validation should fail when scalar components of all user-defined inputs > max vertex shader output components.`
+ )
+ .params(u =>
+ u.combine('isAsync', [false, true]).combineWithParams([
+ // Number of user-defined input scalar components in test shader = device.limits.maxInterStageShaderComponents + numScalarDelta.
+ { numScalarDelta: 0, useExtraBuiltinInputs: false, _success: true },
+ { numScalarDelta: 1, useExtraBuiltinInputs: false, _success: false },
+ { numScalarDelta: 0, useExtraBuiltinInputs: true, _success: false },
+ { numScalarDelta: -3, useExtraBuiltinInputs: true, _success: true },
+ { numScalarDelta: -2, useExtraBuiltinInputs: true, _success: false },
+ ] as const)
+ )
+ .fn(async t => {
+ const { isAsync, numScalarDelta, useExtraBuiltinInputs, _success } = t.params;
+
+ const numScalarComponents = t.device.limits.maxInterStageShaderComponents + numScalarDelta;
+
+ const numVec4 = Math.floor(numScalarComponents / 4);
+ const numTrailingScalars = numScalarComponents % 4;
+ const numUserDefinedInterStageVariables = numTrailingScalars > 0 ? numVec4 + 1 : numVec4;
+
+ assert(numUserDefinedInterStageVariables <= t.device.limits.maxInterStageShaderVariables);
+
+ const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`);
+ const inputs = range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`);
+
+ if (numTrailingScalars > 0) {
+ const typeString = numTrailingScalars === 1 ? 'f32' : `vec${numTrailingScalars}<f32>`;
+ outputs.push(`@location(${numVec4}) vout${numVec4}: ${typeString}`);
+ inputs.push(`@location(${numVec4}) fin${numVec4}: ${typeString}`);
+ }
+
+ if (useExtraBuiltinInputs) {
+ inputs.push(
+ '@builtin(front_facing) front_facing_in: bool',
+ '@builtin(sample_index) sample_index_in: u32',
+ '@builtin(sample_mask) sample_mask_in: u32'
+ );
+ }
+
+ const descriptor = t.getDescriptorWithStates(
+ t.getVertexStateWithOutputs(outputs),
+ t.getFragmentStateWithInputs(inputs, true)
+ );
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts
new file mode 100644
index 0000000000..a046ad86c4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/misc.spec.ts
@@ -0,0 +1,94 @@
+export const description = `
+misc createRenderPipeline and createRenderPipelineAsync validation tests.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kDefaultVertexShaderCode, kDefaultFragmentShaderCode } from '../../../util/shader.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+g.test('basic')
+ .desc(`Test basic usage of createRenderPipeline.`)
+ .params(u => u.combine('isAsync', [false, true]))
+ .fn(async t => {
+ const { isAsync } = t.params;
+ const descriptor = t.getDescriptor();
+
+ t.doCreateRenderPipelineTest(isAsync, true, descriptor);
+ });
+
+g.test('vertex_state_only')
+ .desc(
+ `Tests creating vertex-state-only render pipeline. A vertex-only render pipeline has no fragment
+state (and thus has no color state), and can be created with or without depth stencil state.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .beginSubcases()
+ .combine('depthStencilFormat', [
+ 'depth24plus',
+ 'depth24plus-stencil8',
+ 'depth32float',
+ '',
+ ] as const)
+ .combine('hasColor', [false, true])
+ )
+ .fn(async t => {
+ const { isAsync, depthStencilFormat, hasColor } = t.params;
+
+ let depthStencilState: GPUDepthStencilState | undefined;
+ if (depthStencilFormat === '') {
+ depthStencilState = undefined;
+ } else {
+ depthStencilState = { format: depthStencilFormat };
+ }
+
+ // Having targets or not should have no effect in result, since it will not appear in the
+ // descriptor in vertex-only render pipeline
+ const descriptor = t.getDescriptor({
+ noFragment: true,
+ depthStencil: depthStencilState,
+ targets: hasColor ? [{ format: 'rgba8unorm' }] : [],
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, true, descriptor);
+ });
+
+g.test('pipeline_layout,device_mismatch')
+ .desc(
+ 'Tests createRenderPipeline(Async) cannot be called with a pipeline layout created from another device'
+ )
+ .paramsSubcasesOnly(u => u.combine('isAsync', [true, false]).combine('mismatched', [true, false]))
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { isAsync, mismatched } = t.params;
+
+ const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
+
+ const layout = sourceDevice.createPipelineLayout({ bindGroupLayouts: [] });
+
+ const format = 'rgba8unorm';
+ const descriptor = {
+ layout,
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kDefaultVertexShaderCode,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: kDefaultFragmentShaderCode,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }] as const,
+ },
+ };
+
+ t.doCreateRenderPipelineTest(isAsync, !mismatched, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/multisample_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/multisample_state.spec.ts
new file mode 100644
index 0000000000..053b00e6a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/multisample_state.spec.ts
@@ -0,0 +1,83 @@
+export const description = `
+This test dedicatedly tests validation of GPUMultisampleState of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kDefaultFragmentShaderCode } from '../../../util/shader.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+g.test('count')
+ .desc(`If multisample.count must either be 1 or 4.`)
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .beginSubcases()
+ .combine('count', [0, 1, 2, 3, 4, 8, 16, 1024])
+ )
+ .fn(async t => {
+ const { isAsync, count } = t.params;
+
+ const descriptor = t.getDescriptor({ multisample: { count, alphaToCoverageEnabled: false } });
+
+ const _success = count === 1 || count === 4;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('alpha_to_coverage,count')
+ .desc(
+ `If multisample.alphaToCoverageEnabled is true, multisample.count must be greater than 1, e.g. it can only be 4.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('alphaToCoverageEnabled', [false, true])
+ .beginSubcases()
+ .combine('count', [1, 4])
+ )
+ .fn(async t => {
+ const { isAsync, alphaToCoverageEnabled, count } = t.params;
+
+ const descriptor = t.getDescriptor({ multisample: { count, alphaToCoverageEnabled } });
+
+ const _success = alphaToCoverageEnabled ? count === 4 : count === 1 || count === 4;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('alpha_to_coverage,sample_mask')
+ .desc(
+ `If sample_mask builtin is a pipeline output of fragment, multisample.alphaToCoverageEnabled should be false.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('alphaToCoverageEnabled', [false, true])
+ .beginSubcases()
+ .combine('hasSampleMaskOutput', [false, true])
+ )
+ .fn(async t => {
+ const { isAsync, alphaToCoverageEnabled, hasSampleMaskOutput } = t.params;
+
+ const descriptor = t.getDescriptor({
+ multisample: { alphaToCoverageEnabled, count: 4 },
+ fragmentShaderCode: hasSampleMaskOutput
+ ? `
+ struct Output {
+ @builtin(sample_mask) mask_out: u32,
+ @location(0) color : vec4<f32>,
+ }
+ @fragment fn main() -> Output {
+ var o: Output;
+ // We need to make sure this sample_mask isn't optimized out even its value equals "no op".
+ o.mask_out = 0xFFFFFFFFu;
+ o.color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ return o;
+ }`
+ : kDefaultFragmentShaderCode,
+ });
+
+ const _success = !hasSampleMaskOutput || !alphaToCoverageEnabled;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/overrides.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/overrides.spec.ts
new file mode 100644
index 0000000000..bdf0024722
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/overrides.spec.ts
@@ -0,0 +1,501 @@
+export const description = `
+This test dedicatedly tests validation of pipeline overridable constants of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kValue } from '../../../util/constants.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+g.test('identifier,vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for overridable constants identifiers in vertex state.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { vertexConstants: {}, _success: true },
+ { vertexConstants: { x: 1, y: 1 }, _success: true },
+ { vertexConstants: { x: 1, y: 1, 1: 1, 1000: 1 }, _success: true },
+ { vertexConstants: { xxx: 1 }, _success: false },
+ { vertexConstants: { 1: 1 }, _success: true },
+ { vertexConstants: { 2: 1 }, _success: false },
+ { vertexConstants: { z: 1 }, _success: false }, // pipeline constant id is specified for z
+ { vertexConstants: { w: 1 }, _success: false }, // pipeline constant id is specified for w
+ { vertexConstants: { 1: 1, z: 1 }, _success: false }, // pipeline constant id is specified for z
+ ] as { vertexConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, vertexConstants, _success } = t.params;
+
+ t.doCreateRenderPipelineTest(isAsync, _success, {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ override x: f32 = 0.0;
+ override y: f32 = 0.0;
+ @id(1) override z: f32 = 0.0;
+ @id(1000) override w: f32 = 1.0;
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(x, y, z, w);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
+
+g.test('identifier,fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for overridable constants identifiers in fragment state.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { fragmentConstants: {}, _success: true },
+ { fragmentConstants: { r: 1, g: 1 }, _success: true },
+ { fragmentConstants: { r: 1, g: 1, 1: 1, 1000: 1 }, _success: true },
+ { fragmentConstants: { xxx: 1 }, _success: false },
+ { fragmentConstants: { 1: 1 }, _success: true },
+ { fragmentConstants: { 2: 1 }, _success: false },
+ { fragmentConstants: { b: 1 }, _success: false }, // pipeline constant id is specified for b
+ { fragmentConstants: { a: 1 }, _success: false }, // pipeline constant id is specified for a
+ { fragmentConstants: { 1: 1, b: 1 }, _success: false }, // pipeline constant id is specified for b
+ ] as { fragmentConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, fragmentConstants, _success } = t.params;
+
+ const descriptor = t.getDescriptor({
+ fragmentShaderCode: `
+ override r: f32 = 0.0;
+ override g: f32 = 0.0;
+ @id(1) override b: f32 = 0.0;
+ @id(1000) override a: f32 = 0.0;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ return vec4<f32>(r, g, b, a);
+ }`,
+ fragmentConstants,
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('uninitialized,vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for uninitialized overridable constants in vertex state.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { vertexConstants: {}, _success: false },
+ { vertexConstants: { x: 1, y: 1 }, _success: false }, // z is missing
+ { vertexConstants: { x: 1, z: 1 }, _success: true },
+ { vertexConstants: { x: 1, y: 1, z: 1, w: 1 }, _success: true },
+ ] as { vertexConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, vertexConstants, _success } = t.params;
+
+ t.doCreateRenderPipelineTest(isAsync, _success, {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ override x: f32;
+ override y: f32 = 0.0;
+ override z: f32;
+ override w: f32 = 1.0;
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(x, y, z, w);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
+
+g.test('uninitialized,fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for uninitialized overridable constants in fragment state.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { fragmentConstants: {}, _success: false },
+ { fragmentConstants: { r: 1, g: 1 }, _success: false }, // b is missing
+ { fragmentConstants: { r: 1, b: 1 }, _success: true },
+ { fragmentConstants: { r: 1, g: 1, b: 1, a: 1 }, _success: true },
+ ] as { fragmentConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, fragmentConstants, _success } = t.params;
+
+ const descriptor = t.getDescriptor({
+ fragmentShaderCode: `
+ override r: f32;
+ override g: f32 = 0.0;
+ override b: f32;
+ override a: f32 = 0.0;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ return vec4<f32>(r, g, b, a);
+ }
+ `,
+ fragmentConstants,
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('value,type_error,vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for invalid constant values like inf, NaN will results in TypeError.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { vertexConstants: { cf: 1 }, _success: true }, // control
+ { vertexConstants: { cf: NaN }, _success: false },
+ { vertexConstants: { cf: Number.POSITIVE_INFINITY }, _success: false },
+ { vertexConstants: { cf: Number.NEGATIVE_INFINITY }, _success: false },
+ ] as { vertexConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, vertexConstants, _success } = t.params;
+
+ t.doCreateRenderPipelineTest(
+ isAsync,
+ _success,
+ {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ override cf: f32 = 0.0;
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ _ = cf;
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ },
+ 'TypeError'
+ );
+ });
+
+g.test('value,type_error,fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for invalid constant values like inf, NaN will results in TypeError.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { fragmentConstants: { cf: 1 }, _success: true }, // control
+ { fragmentConstants: { cf: NaN }, _success: false },
+ { fragmentConstants: { cf: Number.POSITIVE_INFINITY }, _success: false },
+ { fragmentConstants: { cf: Number.NEGATIVE_INFINITY }, _success: false },
+ ] as const)
+ )
+ .fn(async t => {
+ const { isAsync, fragmentConstants, _success } = t.params;
+
+ const descriptor = t.getDescriptor({
+ fragmentShaderCode: `
+ override cf: f32 = 0.0;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ _ = cf;
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ }
+ `,
+ fragmentConstants,
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor, 'TypeError');
+ });
+
+g.test('value,validation_error,vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for unrepresentable constant values in vertex stage.
+
+TODO(#2060): test with last_f64_castable.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { vertexConstants: { cu: kValue.u32.min }, _success: true },
+ { vertexConstants: { cu: kValue.u32.min - 1 }, _success: false },
+ { vertexConstants: { cu: kValue.u32.max }, _success: true },
+ { vertexConstants: { cu: kValue.u32.max + 1 }, _success: false },
+ { vertexConstants: { ci: kValue.i32.negative.min }, _success: true },
+ { vertexConstants: { ci: kValue.i32.negative.min - 1 }, _success: false },
+ { vertexConstants: { ci: kValue.i32.positive.max }, _success: true },
+ { vertexConstants: { ci: kValue.i32.positive.max + 1 }, _success: false },
+ { vertexConstants: { cf: kValue.f32.negative.min }, _success: true },
+ { vertexConstants: { cf: kValue.f32.negative.first_f64_not_castable }, _success: false },
+ { vertexConstants: { cf: kValue.f32.positive.max }, _success: true },
+ { vertexConstants: { cf: kValue.f32.positive.first_f64_not_castable }, _success: false },
+ // Conversion to boolean can't fail
+ { vertexConstants: { cb: Number.MAX_VALUE }, _success: true },
+ { vertexConstants: { cb: kValue.i32.negative.min - 1 }, _success: true },
+ ] as { vertexConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, vertexConstants, _success } = t.params;
+
+ t.doCreateRenderPipelineTest(isAsync, _success, {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ override cb: bool = false;
+ override cu: u32 = 0u;
+ override ci: i32 = 0;
+ override cf: f32 = 0.0;
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ _ = cb;
+ _ = cu;
+ _ = ci;
+ _ = cf;
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
+
+g.test('value,validation_error,fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for unrepresentable constant values in fragment stage.
+
+TODO(#2060): test with last_f64_castable.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { fragmentConstants: { cu: kValue.u32.min }, _success: true },
+ { fragmentConstants: { cu: kValue.u32.min - 1 }, _success: false },
+ { fragmentConstants: { cu: kValue.u32.max }, _success: true },
+ { fragmentConstants: { cu: kValue.u32.max + 1 }, _success: false },
+ { fragmentConstants: { ci: kValue.i32.negative.min }, _success: true },
+ { fragmentConstants: { ci: kValue.i32.negative.min - 1 }, _success: false },
+ { fragmentConstants: { ci: kValue.i32.positive.max }, _success: true },
+ { fragmentConstants: { ci: kValue.i32.positive.max + 1 }, _success: false },
+ { fragmentConstants: { cf: kValue.f32.negative.min }, _success: true },
+ { fragmentConstants: { cf: kValue.f32.negative.first_f64_not_castable }, _success: false },
+ { fragmentConstants: { cf: kValue.f32.positive.max }, _success: true },
+ { fragmentConstants: { cf: kValue.f32.positive.first_f64_not_castable }, _success: false },
+ // Conversion to boolean can't fail
+ { fragmentConstants: { cb: Number.MAX_VALUE }, _success: true },
+ { fragmentConstants: { cb: kValue.i32.negative.min - 1 }, _success: true },
+ ] as { fragmentConstants: Record<string, GPUPipelineConstantValue>; _success: boolean }[])
+ )
+ .fn(async t => {
+ const { isAsync, fragmentConstants, _success } = t.params;
+
+ const descriptor = t.getDescriptor({
+ fragmentShaderCode: `
+ override cb: bool = false;
+ override cu: u32 = 0u;
+ override ci: i32 = 0;
+ override cf: f32 = 0.0;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ _ = cb;
+ _ = cu;
+ _ = ci;
+ _ = cf;
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ }
+ `,
+ fragmentConstants,
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('value,validation_error,f16,vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for unrepresentable f16 constant values in vertex stage.
+
+TODO(#2060): Tighten the cases around the valid/invalid boundary once we have WGSL spec
+clarity on whether values like f16.positive.last_f64_castable would be valid. See issue.
+`
+ )
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { vertexConstants: { cf16: kValue.f16.negative.min }, _success: true },
+ { vertexConstants: { cf16: kValue.f16.negative.first_f64_not_castable }, _success: false },
+ { vertexConstants: { cf16: kValue.f16.positive.max }, _success: true },
+ { vertexConstants: { cf16: kValue.f16.positive.first_f64_not_castable }, _success: false },
+ { vertexConstants: { cf16: kValue.f32.negative.min }, _success: false },
+ { vertexConstants: { cf16: kValue.f32.positive.max }, _success: false },
+ { vertexConstants: { cf16: kValue.f32.negative.first_f64_not_castable }, _success: false },
+ { vertexConstants: { cf16: kValue.f32.positive.first_f64_not_castable }, _success: false },
+ ] as const)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] });
+ })
+ .fn(async t => {
+ const { isAsync, vertexConstants, _success } = t.params;
+
+ t.doCreateRenderPipelineTest(isAsync, _success, {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ enable f16;
+
+ override cf16: f16 = 0.0h;
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ _ = cf16;
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ constants: vertexConstants,
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
+
+g.test('value,validation_error,f16,fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) validation for unrepresentable f16 constant values in fragment stage.
+
+TODO(#2060): Tighten the cases around the valid/invalid boundary once we have WGSL spec
+clarity on whether values like f16.positive.last_f64_castable would be valid. See issue.
+`
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] });
+ })
+ .params(u =>
+ u //
+ .combine('isAsync', [true, false])
+ .combineWithParams([
+ { fragmentConstants: { cf16: kValue.f16.negative.min }, _success: true },
+ {
+ fragmentConstants: { cf16: kValue.f16.negative.first_f64_not_castable },
+ _success: false,
+ },
+ { fragmentConstants: { cf16: kValue.f16.positive.max }, _success: true },
+ {
+ fragmentConstants: { cf16: kValue.f16.positive.first_f64_not_castable },
+ _success: false,
+ },
+ { fragmentConstants: { cf16: kValue.f32.negative.min }, _success: false },
+ { fragmentConstants: { cf16: kValue.f32.positive.max }, _success: false },
+ {
+ fragmentConstants: { cf16: kValue.f32.negative.first_f64_not_castable },
+ _success: false,
+ },
+ {
+ fragmentConstants: { cf16: kValue.f32.positive.first_f64_not_castable },
+ _success: false,
+ },
+ ] as const)
+ )
+ .fn(async t => {
+ const { isAsync, fragmentConstants, _success } = t.params;
+
+ const descriptor = t.getDescriptor({
+ fragmentShaderCode: `
+ enable f16;
+
+ override cf16: f16 = 0.0h;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ _ = cf16;
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+ }
+ `,
+ fragmentConstants,
+ });
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/primitive_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/primitive_state.spec.ts
new file mode 100644
index 0000000000..9868bdcac1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/primitive_state.spec.ts
@@ -0,0 +1,42 @@
+export const description = `
+This test dedicatedly tests validation of GPUPrimitiveState of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kPrimitiveTopology, kIndexFormat } from '../../../capability_info.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+g.test('strip_index_format')
+ .desc(
+ `If primitive.topology is not "line-strip" or "triangle-strip", primitive.stripIndexFormat must be undefined.`
+ )
+ .params(u =>
+ u
+ .combine('isAsync', [false, true])
+ .combine('topology', [undefined, ...kPrimitiveTopology] as const)
+ .combine('stripIndexFormat', [undefined, ...kIndexFormat] as const)
+ )
+ .fn(async t => {
+ const { isAsync, topology, stripIndexFormat } = t.params;
+
+ const descriptor = t.getDescriptor({ primitive: { topology, stripIndexFormat } });
+
+ const _success =
+ topology === 'line-strip' || topology === 'triangle-strip' || stripIndexFormat === undefined;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('unclipped_depth')
+ .desc(`If primitive.unclippedDepth is true, features must contain "depth-clip-control".`)
+ .params(u => u.combine('isAsync', [false, true]).combine('unclippedDepth', [false, true]))
+ .fn(async t => {
+ const { isAsync, unclippedDepth } = t.params;
+
+ const descriptor = t.getDescriptor({ primitive: { unclippedDepth } });
+
+ const _success = !unclippedDepth || t.device.features.has('depth-clip-control');
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/shader_module.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/shader_module.spec.ts
new file mode 100644
index 0000000000..050e656346
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/shader_module.spec.ts
@@ -0,0 +1,112 @@
+export const description = `
+This test dedicatedly tests createRenderPipeline validation issues related to the shader modules.
+
+Note: entry point matching tests are in ../shader_module/entry_point.spec.ts
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ getFragmentShaderCodeWithOutput,
+ kDefaultVertexShaderCode,
+ kDefaultFragmentShaderCode,
+} from '../../../util/shader.js';
+
+import { CreateRenderPipelineValidationTest } from './common.js';
+
+export const g = makeTestGroup(CreateRenderPipelineValidationTest);
+
+const values = [0, 1, 0, 1];
+
+g.test('device_mismatch')
+ .desc(
+ 'Tests createRenderPipeline(Async) cannot be called with a shader module created from another device'
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('isAsync', [true, false]).combineWithParams([
+ { vertex_mismatched: false, fragment_mismatched: false, _success: true },
+ { vertex_mismatched: true, fragment_mismatched: false, _success: false },
+ { vertex_mismatched: false, fragment_mismatched: true, _success: false },
+ ])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const { isAsync, vertex_mismatched, fragment_mismatched, _success } = t.params;
+
+ const code = `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+ `;
+
+ const descriptor = {
+ vertex: {
+ module: vertex_mismatched
+ ? t.mismatchedDevice.createShaderModule({ code })
+ : t.device.createShaderModule({ code }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: fragment_mismatched
+ ? t.mismatchedDevice.createShaderModule({
+ code: getFragmentShaderCodeWithOutput([
+ { values, plainType: 'f32', componentCount: 4 },
+ ]),
+ })
+ : t.device.createShaderModule({
+ code: getFragmentShaderCodeWithOutput([
+ { values, plainType: 'f32', componentCount: 4 },
+ ]),
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }] as const,
+ },
+ layout: t.getPipelineLayout(),
+ };
+
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('invalid,vertex')
+ .desc(`Tests shader module must be valid.`)
+ .params(u => u.combine('isAsync', [true, false]).combine('isVertexShaderValid', [true, false]))
+ .fn(async t => {
+ const { isAsync, isVertexShaderValid } = t.params;
+ t.doCreateRenderPipelineTest(isAsync, isVertexShaderValid, {
+ layout: 'auto',
+ vertex: {
+ module: isVertexShaderValid
+ ? t.device.createShaderModule({
+ code: kDefaultVertexShaderCode,
+ })
+ : t.createInvalidShaderModule(),
+ entryPoint: 'main',
+ },
+ });
+ });
+
+g.test('invalid,fragment')
+ .desc(`Tests shader module must be valid.`)
+ .params(u => u.combine('isAsync', [true, false]).combine('isFragmentShaderValid', [true, false]))
+ .fn(async t => {
+ const { isAsync, isFragmentShaderValid } = t.params;
+ t.doCreateRenderPipelineTest(isAsync, isFragmentShaderValid, {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kDefaultVertexShaderCode,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: isFragmentShaderValid
+ ? t.device.createShaderModule({
+ code: kDefaultFragmentShaderCode,
+ })
+ : t.createInvalidShaderModule(),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/vertex_state.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/vertex_state.spec.ts
new file mode 100644
index 0000000000..97d84d4ada
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/render_pipeline/vertex_state.spec.ts
@@ -0,0 +1,649 @@
+export const description = `
+This test dedicatedly tests validation of GPUVertexState of createRenderPipeline.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import {
+ kMaxVertexAttributes,
+ kMaxVertexBufferArrayStride,
+ kMaxVertexBuffers,
+ kVertexFormats,
+ kVertexFormatInfo,
+} from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+const VERTEX_SHADER_CODE_WITH_NO_INPUT = `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+ }
+`;
+
+function addTestAttributes(
+ attributes: GPUVertexAttribute[],
+ {
+ testAttribute,
+ testAttributeAtStart = true,
+ extraAttributeCount = 0,
+ extraAttributeSkippedLocations = [],
+ }: {
+ testAttribute?: GPUVertexAttribute;
+ testAttributeAtStart?: boolean;
+ extraAttributeCount?: Number;
+ extraAttributeSkippedLocations?: Number[];
+ }
+) {
+ // Add a bunch of dummy attributes each with a different location such that none of the locations
+ // are in extraAttributeSkippedLocations
+ let currentLocation = 0;
+ let extraAttribsAdded = 0;
+ while (extraAttribsAdded !== extraAttributeCount) {
+ if (extraAttributeSkippedLocations.includes(currentLocation)) {
+ currentLocation++;
+ continue;
+ }
+
+ attributes.push({ format: 'float32', shaderLocation: currentLocation, offset: 0 });
+ currentLocation++;
+ extraAttribsAdded++;
+ }
+
+ // Add the test attribute at the start or the end of the attributes.
+ if (testAttribute) {
+ if (testAttributeAtStart) {
+ attributes.unshift(testAttribute);
+ } else {
+ attributes.push(testAttribute);
+ }
+ }
+}
+
+class F extends ValidationTest {
+ getDescriptor(
+ buffers: Iterable<GPUVertexBufferLayout>,
+ vertexShaderCode: string
+ ): GPURenderPipelineDescriptor {
+ const descriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({ code: vertexShaderCode }),
+ entryPoint: 'main',
+ buffers,
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ };
+ return descriptor;
+ }
+
+ testVertexState(
+ success: boolean,
+ buffers: Iterable<GPUVertexBufferLayout>,
+ vertexShader: string = VERTEX_SHADER_CODE_WITH_NO_INPUT
+ ) {
+ const vsModule = this.device.createShaderModule({ code: vertexShader });
+ const fsModule = this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`,
+ });
+
+ this.expectValidationError(() => {
+ this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: vsModule,
+ entryPoint: 'main',
+ buffers,
+ },
+ fragment: {
+ module: fsModule,
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ }, !success);
+ }
+
+ generateTestVertexShader(inputs: { type: string; location: number }[]): string {
+ let interfaces = '';
+ let body = '';
+
+ let count = 0;
+ for (const input of inputs) {
+ interfaces += `@location(${input.location}) input${count} : ${input.type},\n`;
+ body += `var i${count} : ${input.type} = input.input${count};\n`;
+ count++;
+ }
+
+ return `
+ struct Inputs {
+ ${interfaces}
+ };
+ @vertex fn main(input : Inputs) -> @builtin(position) vec4<f32> {
+ ${body}
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+ }
+ `;
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('max_vertex_buffer_limit')
+ .desc(
+ `Test that only up to <maxVertexBuffers> vertex buffers are allowed.
+ - Tests with 0, 1, limits, limits + 1 vertex buffers.
+ - Tests with the last buffer having an attribute or not.
+ This also happens to test that vertex buffers with no attributes are allowed and that a vertex state with no buffers is allowed.`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('count', [0, 1, kMaxVertexBuffers, kMaxVertexBuffers + 1])
+ .combine('lastEmpty', [false, true])
+ )
+ .fn(t => {
+ const { count, lastEmpty } = t.params;
+
+ const vertexBuffers = [];
+ for (let i = 0; i < count; i++) {
+ if (lastEmpty || i !== count - 1) {
+ vertexBuffers.push({ attributes: [], arrayStride: 0 });
+ } else {
+ vertexBuffers.push({
+ attributes: [{ format: 'float32', offset: 0, shaderLocation: 0 }],
+ arrayStride: 0,
+ } as const);
+ }
+ }
+
+ const success = count <= kMaxVertexBuffers;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('max_vertex_attribute_limit')
+ .desc(
+ `Test that only up to <maxVertexAttributes> vertex attributes are allowed.
+ - Tests with 0, 1, limit, limits + 1 vertex attribute.
+ - Tests with 0, 1, 4 attributes per buffer (with remaining attributes in the last buffer).`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('attribCount', [0, 1, kMaxVertexAttributes, kMaxVertexAttributes + 1])
+ .combine('attribsPerBuffer', [0, 1, 4])
+ )
+ .fn(t => {
+ const { attribCount, attribsPerBuffer } = t.params;
+
+ const vertexBuffers = [];
+
+ let attribsAdded = 0;
+ while (attribsAdded !== attribCount) {
+ // Choose how many attributes to add for this buffer. The last buffer gets all remaining attributes.
+ let targetCount = Math.min(attribCount, attribsAdded + attribsPerBuffer);
+ if (vertexBuffers.length === kMaxVertexBuffers - 1) {
+ targetCount = attribCount;
+ }
+
+ const attributes = [];
+ while (attribsAdded !== targetCount) {
+ attributes.push({ format: 'float32', offset: 0, shaderLocation: attribsAdded } as const);
+ attribsAdded++;
+ }
+
+ vertexBuffers.push({ arrayStride: 0, attributes });
+ }
+
+ const success = attribCount <= kMaxVertexAttributes;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('max_vertex_buffer_array_stride_limit')
+ .desc(
+ `Test that the vertex buffer arrayStride must be at most <maxVertexBufferArrayStride>.
+ - Test for various vertex buffer indices
+ - Test for array strides 0, 4, 256, limit - 4, limit, limit + 4`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('arrayStride', [
+ 0,
+ 4,
+ 256,
+ kMaxVertexBufferArrayStride - 4,
+ kMaxVertexBufferArrayStride,
+ kMaxVertexBufferArrayStride + 4,
+ ])
+ )
+ .fn(t => {
+ const { vertexBufferIndex, arrayStride } = t.params;
+
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride, attributes: [] };
+
+ const success = arrayStride <= kMaxVertexBufferArrayStride;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('vertex_buffer_array_stride_limit_alignment')
+ .desc(
+ `Test that the vertex buffer arrayStride must be a multiple of 4 (including 0).
+ - Test for various vertex buffer indices
+ - Test for array strides 0, 1, 2, 4, limit - 4, limit - 2, limit`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('arrayStride', [
+ 0,
+ 1,
+ 2,
+ 4,
+ kMaxVertexBufferArrayStride - 4,
+ kMaxVertexBufferArrayStride - 2,
+ kMaxVertexBufferArrayStride,
+ ])
+ )
+ .fn(t => {
+ const { vertexBufferIndex, arrayStride } = t.params;
+
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride, attributes: [] };
+
+ const success = arrayStride % 4 === 0;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('vertex_attribute_shaderLocation_limit')
+ .desc(
+ `Test shaderLocation must be less than maxVertexAttributes.
+ - Test for various vertex buffer indices
+ - Test for various amounts of attributes in that vertex buffer
+ - Test for shaderLocation 0, 1, limit - 1, limit`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('extraAttributeCount', [0, 1, kMaxVertexAttributes - 1])
+ .combine('testAttributeAtStart', [false, true])
+ .combine('testShaderLocation', [0, 1, kMaxVertexAttributes - 1, kMaxVertexAttributes])
+ )
+ .fn(t => {
+ const {
+ vertexBufferIndex,
+ extraAttributeCount,
+ testShaderLocation,
+ testAttributeAtStart,
+ } = t.params;
+
+ const attributes: GPUVertexAttribute[] = [];
+ addTestAttributes(attributes, {
+ testAttribute: { format: 'float32', offset: 0, shaderLocation: testShaderLocation },
+ testAttributeAtStart,
+ extraAttributeCount,
+ extraAttributeSkippedLocations: [testShaderLocation],
+ });
+
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride: 256, attributes };
+
+ const success = testShaderLocation < kMaxVertexAttributes;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('vertex_attribute_shaderLocation_unique')
+ .desc(
+ `Test that shaderLocation must be unique in the vertex state.
+ - Test for various pairs of buffers that contain the potentially conflicting attributes
+ - Test for the potentially conflicting attributes in various places in the buffers (with dummy attributes)
+ - Test for various shaderLocations that conflict or not`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('vertexBufferIndexA', [0, 1, kMaxVertexBuffers - 1])
+ .combine('vertexBufferIndexB', [0, 1, kMaxVertexBuffers - 1])
+ .combine('testAttributeAtStartA', [false, true])
+ .combine('testAttributeAtStartB', [false, true])
+ .combine('shaderLocationA', [0, 1, 7, kMaxVertexAttributes - 1])
+ .combine('shaderLocationB', [0, 1, 7, kMaxVertexAttributes - 1])
+ .combine('extraAttributeCount', [0, 4])
+ )
+ .fn(t => {
+ const {
+ vertexBufferIndexA,
+ vertexBufferIndexB,
+ testAttributeAtStartA,
+ testAttributeAtStartB,
+ shaderLocationA,
+ shaderLocationB,
+ extraAttributeCount,
+ } = t.params;
+
+ // Depending on the params, the vertexBuffer for A and B can be the same or different. To support
+ // both cases without code changes we treat `vertexBufferAttributes` as a map from indices to
+ // vertex buffer descriptors, with A and B potentially reusing the same JS object if they have the
+ // same index.
+ const vertexBufferAttributes = [];
+ vertexBufferAttributes[vertexBufferIndexA] = [];
+ vertexBufferAttributes[vertexBufferIndexB] = [];
+
+ // Add the dummy attributes for attribute A
+ const attributesA = vertexBufferAttributes[vertexBufferIndexA];
+ addTestAttributes(attributesA, {
+ testAttribute: { format: 'float32', offset: 0, shaderLocation: shaderLocationA },
+ testAttributeAtStart: testAttributeAtStartA,
+ extraAttributeCount,
+ extraAttributeSkippedLocations: [shaderLocationA, shaderLocationB],
+ });
+
+ // Add attribute B. Not that attributesB can be the same object as attributesA so they end
+ // up in the same vertex buffer.
+ const attributesB = vertexBufferAttributes[vertexBufferIndexB];
+ addTestAttributes(attributesB, {
+ testAttribute: { format: 'float32', offset: 0, shaderLocation: shaderLocationB },
+ testAttributeAtStart: testAttributeAtStartB,
+ });
+
+ // Use the attributes to make the list of vertex buffers. Note that we might be setting the same vertex
+ // buffer twice, but that only happens when it is the only vertex buffer.
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndexA] = { arrayStride: 256, attributes: attributesA };
+ vertexBuffers[vertexBufferIndexB] = { arrayStride: 256, attributes: attributesB };
+
+ // Note that an empty vertex shader will be used so errors only happens because of the conflict
+ // in the vertex state.
+ const success = shaderLocationA !== shaderLocationB;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('vertex_shader_input_location_limit')
+ .desc(
+ `Test that vertex shader's input's location decoration must be less than maxVertexAttributes.
+ - Test for shaderLocation 0, 1, limit - 1, limit, MAX_I32 (the WGSL spec requires a non-negative i32)`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('testLocation', [0, 1, kMaxVertexAttributes - 1, kMaxVertexAttributes, 2 ** 31 - 1])
+ )
+ .fn(t => {
+ const { testLocation } = t.params;
+
+ const shader = t.generateTestVertexShader([
+ {
+ type: 'vec4<f32>',
+ location: testLocation,
+ },
+ ]);
+
+ const vertexBuffers = [
+ {
+ arrayStride: 512,
+ attributes: [
+ {
+ format: 'float32',
+ offset: 0,
+ shaderLocation: testLocation,
+ } as const,
+ ],
+ },
+ ];
+
+ const success = testLocation < kMaxVertexAttributes;
+ t.testVertexState(success, vertexBuffers, shader);
+ });
+
+g.test('vertex_shader_input_location_in_vertex_state')
+ .desc(
+ `Test that a vertex shader defined in the shader must have a corresponding attribute in the vertex state.
+ - Test for various input locations.
+ - Test for the attribute in various places in the list of vertex buffer and various places inside the vertex buffer descriptor`
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('extraAttributeCount', [0, 1, kMaxVertexAttributes - 1])
+ .combine('testAttributeAtStart', [false, true])
+ .combine('testShaderLocation', [0, 1, 4, 7, kMaxVertexAttributes - 1])
+ )
+ .fn(t => {
+ const {
+ vertexBufferIndex,
+ extraAttributeCount,
+ testAttributeAtStart,
+ testShaderLocation,
+ } = t.params;
+ // We have a shader using `testShaderLocation`.
+ const shader = t.generateTestVertexShader([
+ {
+ type: 'vec4<f32>',
+ location: testShaderLocation,
+ },
+ ]);
+
+ const attributes: GPUVertexAttribute[] = [];
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride: 256, attributes };
+
+ // Fill attributes with a bunch of attributes for other locations.
+ // Using that vertex state is invalid because the vertex state doesn't contain the test location
+ addTestAttributes(attributes, {
+ extraAttributeCount,
+ extraAttributeSkippedLocations: [testShaderLocation],
+ });
+ t.testVertexState(false, vertexBuffers, shader);
+
+ // Add an attribute for the test location and try again.
+ addTestAttributes(attributes, {
+ testAttribute: { format: 'float32', shaderLocation: testShaderLocation, offset: 0 },
+ testAttributeAtStart,
+ });
+ t.testVertexState(true, vertexBuffers, shader);
+ });
+
+g.test('vertex_shader_type_matches_attribute_format')
+ .desc(
+ `
+ Test that the vertex shader declaration must have a type compatible with the vertex format.
+ - Test for all formats.
+ - Test for all combinations of u/i/f32 with and without vectors.`
+ )
+ .params(u =>
+ u
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('shaderBaseType', ['u32', 'i32', 'f32'])
+ .expand('shaderType', p => [
+ p.shaderBaseType,
+ `vec2<${p.shaderBaseType}>`,
+ `vec3<${p.shaderBaseType}>`,
+ `vec4<${p.shaderBaseType}>`,
+ ])
+ )
+ .fn(t => {
+ const { format, shaderBaseType, shaderType } = t.params;
+ const shader = t.generateTestVertexShader([
+ {
+ type: shaderType,
+ location: 0,
+ },
+ ]);
+
+ const requiredBaseType = {
+ sint: 'i32',
+ uint: 'u32',
+ snorm: 'f32',
+ unorm: 'f32',
+ float: 'f32',
+ }[kVertexFormatInfo[format].type];
+
+ const success = requiredBaseType === shaderBaseType;
+ t.testVertexState(
+ success,
+ [
+ {
+ arrayStride: 0,
+ attributes: [{ offset: 0, shaderLocation: 0, format }],
+ },
+ ],
+ shader
+ );
+ });
+
+g.test('vertex_attribute_offset_alignment')
+ .desc(
+ `
+ Test that vertex attribute offsets must be aligned to the format's component byte size.
+ - Test for all formats.
+ - Test for various arrayStrides and offsets within that stride
+ - Test for various vertex buffer indices
+ - Test for various amounts of attributes in that vertex buffer`
+ )
+ .params(u =>
+ u
+ .combine('format', kVertexFormats)
+ .combine('arrayStride', [256, kMaxVertexBufferArrayStride])
+ .expand('offset', p => {
+ const { bytesPerComponent, componentCount } = kVertexFormatInfo[p.format];
+ const formatSize = bytesPerComponent * componentCount;
+
+ return new Set([
+ 0,
+ Math.floor(formatSize / 2),
+ formatSize,
+ 2,
+ 4,
+ p.arrayStride - formatSize,
+ p.arrayStride - formatSize - Math.floor(formatSize / 2),
+ p.arrayStride - formatSize - 4,
+ p.arrayStride - formatSize - 2,
+ ]);
+ })
+ .beginSubcases()
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('extraAttributeCount', [0, 1, kMaxVertexAttributes - 1])
+ .combine('testAttributeAtStart', [false, true])
+ )
+ .fn(t => {
+ const {
+ format,
+ arrayStride,
+ offset,
+ vertexBufferIndex,
+ extraAttributeCount,
+ testAttributeAtStart,
+ } = t.params;
+
+ const attributes: GPUVertexAttribute[] = [];
+ addTestAttributes(attributes, {
+ testAttribute: { format, offset, shaderLocation: 0 },
+ testAttributeAtStart,
+ extraAttributeCount,
+ extraAttributeSkippedLocations: [0],
+ });
+
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride, attributes };
+
+ const formatInfo = kVertexFormatInfo[format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ const success = offset % Math.min(4, formatSize) === 0;
+
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('vertex_attribute_contained_in_stride')
+ .desc(
+ `
+ Test that vertex attribute [offset, offset + formatSize) must be contained in the arrayStride if arrayStride is not 0:
+ - Test for all formats.
+ - Test for various arrayStrides and offsets within that stride
+ - Test for various vertex buffer indices
+ - Test for various amounts of attributes in that vertex buffer`
+ )
+ .params(u =>
+ u
+ .combine('format', kVertexFormats)
+ .beginSubcases()
+ .combine('arrayStride', [
+ 0,
+ 256,
+ kMaxVertexBufferArrayStride - 4,
+ kMaxVertexBufferArrayStride,
+ ])
+ .expand('offset', function* (p) {
+ // Compute a bunch of test offsets to test.
+ const { bytesPerComponent, componentCount } = kVertexFormatInfo[p.format];
+ const formatSize = bytesPerComponent * componentCount;
+ yield 0;
+ yield 4;
+
+ // arrayStride = 0 is a special case because for the offset validation it acts the same
+ // as arrayStride = kMaxVertexBufferArrayStride. We special case here so as to avoid adding
+ // negative offsets that would cause an IDL exception to be thrown instead of a validation
+ // error.
+ const stride = p.arrayStride !== 0 ? p.arrayStride : kMaxVertexBufferArrayStride;
+ yield stride - formatSize;
+ yield stride - formatSize + 4;
+
+ // Avoid adding duplicate cases when formatSize == 4 (it is already tested above)
+ if (formatSize !== 4) {
+ yield formatSize;
+ yield stride;
+ }
+ })
+ .combine('vertexBufferIndex', [0, 1, kMaxVertexBuffers - 1])
+ .combine('extraAttributeCount', [0, 1, kMaxVertexAttributes - 1])
+ .combine('testAttributeAtStart', [false, true])
+ )
+ .fn(t => {
+ const {
+ format,
+ arrayStride,
+ offset,
+ vertexBufferIndex,
+ extraAttributeCount,
+ testAttributeAtStart,
+ } = t.params;
+
+ const attributes: GPUVertexAttribute[] = [];
+ addTestAttributes(attributes, {
+ testAttribute: { format, offset, shaderLocation: 0 },
+ testAttributeAtStart,
+ extraAttributeCount,
+ extraAttributeSkippedLocations: [0],
+ });
+
+ const vertexBuffers = [];
+ vertexBuffers[vertexBufferIndex] = { arrayStride, attributes };
+
+ const formatInfo = kVertexFormatInfo[format];
+ const formatSize = formatInfo.bytesPerComponent * formatInfo.componentCount;
+ const limit = arrayStride === 0 ? kMaxVertexBufferArrayStride : arrayStride;
+
+ const success = offset + formatSize <= limit;
+ t.testVertexState(success, vertexBuffers);
+ });
+
+g.test('many_attributes_overlapping')
+ .desc(`Test that it is valid to have many vertex attributes overlap`)
+ .fn(async t => {
+ // Create many attributes, each of them intersects with at least 3 others.
+ const attributes = [];
+ const formats = ['float32x4', 'uint32x4', 'sint32x4'] as const;
+ for (let i = 0; i < kMaxVertexAttributes; i++) {
+ attributes.push({ format: formats[i % 3], offset: i * 4, shaderLocation: i } as const);
+ }
+
+ t.testVertexState(true, [{ arrayStride: 0, attributes }]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/README.txt
new file mode 100644
index 0000000000..e3aa0bb9e7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/README.txt
@@ -0,0 +1 @@
+TODO: look at texture,*
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_encoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_encoder.spec.ts
new file mode 100644
index 0000000000..01ac34bfda
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_encoder.spec.ts
@@ -0,0 +1,910 @@
+export const description = `
+Buffer Usages Validation Tests in Render Pass and Compute Pass.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../../../common/util/util.js';
+import { ValidationTest } from '../../validation_test.js';
+
+const kBoundBufferSize = 256;
+
+export type BufferUsage =
+ | 'uniform'
+ | 'storage'
+ | 'read-only-storage'
+ | 'vertex'
+ | 'index'
+ | 'indirect'
+ | 'indexedIndirect';
+
+export const kAllBufferUsages: BufferUsage[] = [
+ 'uniform',
+ 'storage',
+ 'read-only-storage',
+ 'vertex',
+ 'index',
+ 'indirect',
+ 'indexedIndirect',
+];
+
+export class BufferResourceUsageTest extends ValidationTest {
+ createBindGroupLayoutForTest(
+ type: 'uniform' | 'storage' | 'read-only-storage',
+ resourceVisibility: 'compute' | 'fragment'
+ ): GPUBindGroupLayout {
+ const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = {
+ binding: 0,
+ visibility:
+ resourceVisibility === 'compute' ? GPUShaderStage.COMPUTE : GPUShaderStage.FRAGMENT,
+ buffer: {
+ type,
+ },
+ };
+ return this.device.createBindGroupLayout({
+ entries: [bindGroupLayoutEntry],
+ });
+ }
+
+ createBindGroupForTest(
+ buffer: GPUBuffer,
+ offset: number,
+ type: 'uniform' | 'storage' | 'read-only-storage',
+ resourceVisibility: 'compute' | 'fragment'
+ ): GPUBindGroup {
+ return this.device.createBindGroup({
+ layout: this.createBindGroupLayoutForTest(type, resourceVisibility),
+ entries: [
+ {
+ binding: 0,
+ resource: { buffer, offset, size: kBoundBufferSize },
+ },
+ ],
+ });
+ }
+
+ beginSimpleRenderPass(encoder: GPUCommandEncoder) {
+ const colorTexture = this.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [16, 16, 1],
+ });
+ return encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+
+ createRenderPipelineForTest(
+ pipelineLayout: GPUPipelineLayout | GPUAutoLayoutMode,
+ vertexBufferCount: number
+ ): GPURenderPipeline {
+ const vertexBuffers: GPUVertexBufferLayout[] = [];
+ for (let i = 0; i < vertexBufferCount; ++i) {
+ vertexBuffers.push({
+ arrayStride: 4,
+ attributes: [
+ {
+ format: 'float32',
+ shaderLocation: i,
+ offset: 0,
+ },
+ ],
+ });
+ }
+
+ return this.device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: this.device.createShaderModule({
+ code: this.getNoOpShaderCode('VERTEX'),
+ }),
+ entryPoint: 'main',
+ buffers: vertexBuffers,
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ }
+}
+
+function IsBufferUsageInBindGroup(bufferUsage: BufferUsage): boolean {
+ switch (bufferUsage) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage':
+ return true;
+ case 'vertex':
+ case 'index':
+ case 'indirect':
+ case 'indexedIndirect':
+ return false;
+ default:
+ unreachable();
+ }
+}
+
+export const g = makeTestGroup(BufferResourceUsageTest);
+
+g.test('subresources,buffer_usage_in_one_compute_pass_with_no_dispatch')
+ .desc(
+ `
+Test that it is always allowed to set multiple bind groups with same buffer in a compute pass
+encoder without any dispatch calls as state-setting compute pass commands, like setBindGroup(index,
+bindGroup, dynamicOffsets), do not contribute directly to a usage scope.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage'] as const)
+ .combine('usage1', ['uniform', 'storage', 'read-only-storage'] as const)
+ .beginSubcases()
+ .combine('visibility0', ['compute', 'fragment'] as const)
+ .combine('visibility1', ['compute', 'fragment'] as const)
+ .combine('hasOverlap', [true, false])
+ )
+ .fn(async t => {
+ const { usage0, usage1, visibility0, visibility1, hasOverlap } = t.params;
+
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const computePassEncoder = encoder.beginComputePass();
+
+ const offset0 = 0;
+ const bindGroup0 = t.createBindGroupForTest(buffer, offset0, usage0, visibility0);
+ computePassEncoder.setBindGroup(0, bindGroup0);
+
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ const bindGroup1 = t.createBindGroupForTest(buffer, offset1, usage1, visibility1);
+ computePassEncoder.setBindGroup(1, bindGroup1);
+
+ computePassEncoder.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
+
+g.test('subresources,buffer_usage_in_one_compute_pass_with_one_dispatch')
+ .desc(
+ `
+Test that when one buffer is used in one compute pass encoder, its list of internal usages within
+one usage scope can only be a compatible usage list. According to WebGPU SPEC, within one dispatch,
+for each bind group slot that is used by the current GPUComputePipeline's layout, every subresource
+referenced by that bind group is "used" in the usage scope. `
+ )
+ .params(u =>
+ u
+ .combine('usage0AccessibleInDispatch', [true, false])
+ .combine('usage1AccessibleInDispatch', [true, false])
+ .combine('dispatchBeforeUsage1', [true, false])
+ .beginSubcases()
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage', 'indirect'] as const)
+ .combine('visibility0', ['compute', 'fragment'] as const)
+ .filter(t => {
+ // The buffer with `indirect` usage is always accessible in the dispatch call.
+ if (
+ t.usage0 === 'indirect' &&
+ (!t.usage0AccessibleInDispatch || t.visibility0 !== 'compute' || !t.dispatchBeforeUsage1)
+ ) {
+ return false;
+ }
+ if (t.usage0AccessibleInDispatch && t.visibility0 !== 'compute') {
+ return false;
+ }
+ if (t.dispatchBeforeUsage1 && t.usage1AccessibleInDispatch) {
+ return false;
+ }
+ return true;
+ })
+ .combine('usage1', ['uniform', 'storage', 'read-only-storage', 'indirect'] as const)
+ .combine('visibility1', ['compute', 'fragment'] as const)
+ .filter(t => {
+ if (
+ t.usage1 === 'indirect' &&
+ (!t.usage1AccessibleInDispatch || t.visibility1 !== 'compute' || t.dispatchBeforeUsage1)
+ ) {
+ return false;
+ }
+ // When the first buffer usage is `indirect`, there has already been one dispatch call, so
+ // in this test we always make the second usage inaccessible in the dispatch call.
+ if (
+ t.usage1AccessibleInDispatch &&
+ (t.visibility1 !== 'compute' || t.usage0 === 'indirect')
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .combine('hasOverlap', [true, false])
+ )
+ .fn(async t => {
+ const {
+ usage0AccessibleInDispatch,
+ usage1AccessibleInDispatch,
+ dispatchBeforeUsage1,
+ usage0,
+ visibility0,
+ usage1,
+ visibility1,
+ hasOverlap,
+ } = t.params;
+
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const computePassEncoder = encoder.beginComputePass();
+
+ const offset0 = 0;
+ switch (usage0) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup0 = t.createBindGroupForTest(buffer, offset0, usage0, visibility0);
+ computePassEncoder.setBindGroup(0, bindGroup0);
+
+ /*
+ * setBindGroup(bindGroup0);
+ * dispatchWorkgroups();
+ * setBindGroup(bindGroup1);
+ */
+ if (dispatchBeforeUsage1) {
+ let pipelineLayout: GPUPipelineLayout | undefined = undefined;
+ if (usage0AccessibleInDispatch) {
+ const bindGroupLayout0 = t.createBindGroupLayoutForTest(usage0, visibility0);
+ pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout0],
+ });
+ }
+ const computePipeline = t.createNoOpComputePipeline(pipelineLayout);
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroups(1);
+ }
+ break;
+ }
+ case 'indirect': {
+ /*
+ * dispatchWorkgroupsIndirect(buffer);
+ * setBindGroup(bindGroup1);
+ */
+ assert(dispatchBeforeUsage1);
+ const computePipeline = t.createNoOpComputePipeline();
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroupsIndirect(buffer, offset0);
+ break;
+ }
+ }
+
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ switch (usage1) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup1 = t.createBindGroupForTest(buffer, offset1, usage1, visibility1);
+ const bindGroupIndex = usage0AccessibleInDispatch ? 1 : 0;
+ computePassEncoder.setBindGroup(bindGroupIndex, bindGroup1);
+
+ /*
+ * setBindGroup(bindGroup0);
+ * setBindGroup(bindGroup1);
+ * dispatchWorkgroups();
+ */
+ if (!dispatchBeforeUsage1) {
+ const bindGroupLayouts: GPUBindGroupLayout[] = [];
+ if (usage0AccessibleInDispatch && usage0 !== 'indirect') {
+ const bindGroupLayout0 = t.createBindGroupLayoutForTest(usage0, visibility0);
+ bindGroupLayouts.push(bindGroupLayout0);
+ }
+ if (usage1AccessibleInDispatch) {
+ const bindGroupLayout1 = t.createBindGroupLayoutForTest(usage1, visibility1);
+ bindGroupLayouts.push(bindGroupLayout1);
+ }
+ const pipelineLayout: GPUPipelineLayout | undefined = bindGroupLayouts
+ ? t.device.createPipelineLayout({
+ bindGroupLayouts,
+ })
+ : undefined;
+ const computePipeline = t.createNoOpComputePipeline(pipelineLayout);
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroups(1);
+ }
+ break;
+ }
+ case 'indirect': {
+ /*
+ * setBindGroup(bindGroup0);
+ * dispatchWorkgroupsIndirect(buffer);
+ */
+ assert(!dispatchBeforeUsage1);
+ let pipelineLayout: GPUPipelineLayout | undefined = undefined;
+ if (usage0AccessibleInDispatch) {
+ assert(usage0 !== 'indirect');
+ pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [t.createBindGroupLayoutForTest(usage0, visibility0)],
+ });
+ }
+ const computePipeline = t.createNoOpComputePipeline(pipelineLayout);
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroupsIndirect(buffer, offset1);
+ break;
+ }
+ }
+ computePassEncoder.end();
+
+ const usageHasConflict =
+ (usage0 === 'storage' && usage1 !== 'storage') ||
+ (usage0 !== 'storage' && usage1 === 'storage');
+ const fail =
+ usageHasConflict &&
+ visibility0 === 'compute' &&
+ visibility1 === 'compute' &&
+ usage0AccessibleInDispatch &&
+ usage1AccessibleInDispatch;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, fail);
+ });
+
+g.test('subresources,buffer_usage_in_compute_pass_with_two_dispatches')
+ .desc(
+ `
+Test that it is always allowed to use one buffer in different dispatch calls as in WebGPU SPEC,
+within one dispatch, for each bind group slot that is used by the current GPUComputePipeline's
+layout, every subresource referenced by that bind group is "used" in the usage scope, and different
+dispatch calls refer to different usage scopes.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage', 'indirect'] as const)
+ .combine('usage1', ['uniform', 'storage', 'read-only-storage', 'indirect'] as const)
+ .beginSubcases()
+ .combine('inSamePass', [true, false])
+ .combine('hasOverlap', [true, false])
+ )
+ .fn(async t => {
+ const { usage0, usage1, inSamePass, hasOverlap } = t.params;
+
+ const UseBufferOnComputePassEncoder = (
+ computePassEncoder: GPUComputePassEncoder,
+ buffer: GPUBuffer,
+ usage: 'uniform' | 'storage' | 'read-only-storage' | 'indirect',
+ offset: number
+ ) => {
+ switch (usage) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup = t.createBindGroupForTest(buffer, offset, usage, 'compute');
+ computePassEncoder.setBindGroup(0, bindGroup);
+
+ const bindGroupLayout = t.createBindGroupLayoutForTest(usage, 'compute');
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ });
+ const computePipeline = t.createNoOpComputePipeline(pipelineLayout);
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroups(1);
+ break;
+ }
+ case 'indirect': {
+ const computePipeline = t.createNoOpComputePipeline();
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroupsIndirect(buffer, offset);
+ break;
+ }
+ default:
+ unreachable();
+ break;
+ }
+ };
+
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const computePassEncoder = encoder.beginComputePass();
+
+ const offset0 = 0;
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ UseBufferOnComputePassEncoder(computePassEncoder, buffer, usage0, offset0);
+
+ if (inSamePass) {
+ UseBufferOnComputePassEncoder(computePassEncoder, buffer, usage1, offset1);
+ computePassEncoder.end();
+ } else {
+ computePassEncoder.end();
+ const anotherComputePassEncoder = encoder.beginComputePass();
+ UseBufferOnComputePassEncoder(anotherComputePassEncoder, buffer, usage1, offset1);
+ anotherComputePassEncoder.end();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
+
+g.test('subresources,buffer_usage_in_one_render_pass_with_no_draw')
+ .desc(
+ `
+Test that when one buffer is used in one render pass encoder, its list of internal usages within one
+usage scope (all the commands in the whole render pass) can only be a compatible usage list even if
+there is no draw call in the render pass.
+ `
+ )
+ .params(u =>
+ u
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage', 'vertex', 'index'] as const)
+ .combine('usage1', ['uniform', 'storage', 'read-only-storage', 'vertex', 'index'] as const)
+ .beginSubcases()
+ .combine('hasOverlap', [true, false])
+ .combine('visibility0', ['compute', 'fragment'] as const)
+ .unless(t => t.visibility0 === 'compute' && !IsBufferUsageInBindGroup(t.usage0))
+ .combine('visibility1', ['compute', 'fragment'] as const)
+ .unless(t => t.visibility1 === 'compute' && !IsBufferUsageInBindGroup(t.usage1))
+ )
+ .fn(async t => {
+ const { usage0, usage1, hasOverlap, visibility0, visibility1 } = t.params;
+
+ const UseBufferOnRenderPassEncoder = (
+ buffer: GPUBuffer,
+ offset: number,
+ type: BufferUsage,
+ bindGroupVisibility: 'compute' | 'fragment',
+ renderPassEncoder: GPURenderPassEncoder
+ ) => {
+ switch (type) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup = t.createBindGroupForTest(buffer, offset, type, bindGroupVisibility);
+ renderPassEncoder.setBindGroup(0, bindGroup);
+ break;
+ }
+ case 'vertex': {
+ renderPassEncoder.setVertexBuffer(0, buffer, offset, kBoundBufferSize);
+ break;
+ }
+ case 'index': {
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16', offset, kBoundBufferSize);
+ break;
+ }
+ case 'indirect':
+ case 'indexedIndirect':
+ unreachable();
+ break;
+ }
+ };
+
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage:
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ const offset0 = 0;
+ UseBufferOnRenderPassEncoder(buffer, offset0, usage0, visibility0, renderPassEncoder);
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ UseBufferOnRenderPassEncoder(buffer, offset1, usage1, visibility1, renderPassEncoder);
+ renderPassEncoder.end();
+
+ const fail = (usage0 === 'storage') !== (usage1 === 'storage');
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, fail);
+ });
+
+g.test('subresources,buffer_usage_in_one_render_pass_with_one_draw')
+ .desc(
+ `
+Test that when one buffer is used in one render pass encoder where there is one draw call, its list
+of internal usages within one usage scope (all the commands in the whole render pass) can only be a
+compatible usage list. The usage scope rules are not related to the buffer offset or the bind group
+layout visibilities.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', kAllBufferUsages)
+ .combine('usage1', kAllBufferUsages)
+ .beginSubcases()
+ .combine('usage0AccessibleInDraw', [true, false])
+ .combine('usage1AccessibleInDraw', [true, false])
+ .combine('drawBeforeUsage1', [true, false])
+ .combine('visibility0', ['compute', 'fragment'] as const)
+ .filter(t => {
+ // The buffer with `indirect` or `indexedIndirect` usage is always accessible in the draw
+ // call.
+ if (
+ (t.usage0 === 'indirect' || t.usage0 === 'indexedIndirect') &&
+ (!t.usage0AccessibleInDraw || t.visibility0 !== 'fragment' || !t.drawBeforeUsage1)
+ ) {
+ return false;
+ }
+ // The buffer usages `vertex` and `index` do nothing with shader visibilities.
+ if ((t.usage0 === 'vertex' || t.usage0 === 'index') && t.visibility0 !== 'fragment') {
+ return false;
+ }
+
+ // As usage0 is accessible in the draw call, visibility0 can only be 'fragment'.
+ if (t.usage0AccessibleInDraw && t.visibility0 !== 'fragment') {
+ return false;
+ }
+ // As usage1 is accessible in the draw call, the draw call cannot be before usage1.
+ if (t.drawBeforeUsage1 && t.usage1AccessibleInDraw) {
+ return false;
+ }
+ return true;
+ })
+ .combine('visibility1', ['compute', 'fragment'] as const)
+ .filter(t => {
+ if (
+ (t.usage1 === 'indirect' || t.usage1 === 'indexedIndirect') &&
+ (!t.usage1AccessibleInDraw || t.visibility1 !== 'fragment' || t.drawBeforeUsage1)
+ ) {
+ return false;
+ }
+ if ((t.usage1 === 'vertex' || t.usage1 === 'index') && t.visibility1 !== 'fragment') {
+ return false;
+ }
+ // When the first buffer usage is `indirect` or `indexedIndirect`, there has already been
+ // one draw call, so in this test we always make the second usage inaccessible in the draw
+ // call.
+ if (
+ t.usage1AccessibleInDraw &&
+ (t.visibility1 !== 'fragment' ||
+ t.usage0 === 'indirect' ||
+ t.usage0 === 'indexedIndirect')
+ ) {
+ return false;
+ }
+ // When the first buffer usage is `index` and is accessible in the draw call, the second
+ // usage cannot be `indirect` (it should be `indexedIndirect` for the tests on indirect draw
+ // calls)
+ if (t.usage0 === 'index' && t.usage0AccessibleInDraw && t.usage1 === 'indirect') {
+ return false;
+ }
+ return true;
+ })
+ .combine('hasOverlap', [true, false])
+ )
+ .fn(async t => {
+ const {
+ // Buffer with usage0 will be "used" in the draw call if this value is true.
+ usage0AccessibleInDraw,
+ // Buffer with usage1 will be "used" in the draw call if this value is true.
+ usage1AccessibleInDraw,
+ // Whether we will have the draw call before setting the buffer usage as "usage1" or not.
+ // If it is true: set-usage0 -> draw -> set-usage1 or indirect-draw -> set-usage1
+ // Otherwise: set-usage0 -> set-usage1 -> draw or set-usage0 -> indirect-draw
+ drawBeforeUsage1,
+ usage0,
+ visibility0,
+ usage1,
+ visibility1,
+ hasOverlap,
+ } = t.params;
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage:
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX |
+ GPUBufferUsage.INDIRECT,
+ });
+
+ const UseBufferOnRenderPassEncoder = (
+ bufferAccessibleInDraw: boolean,
+ bufferIndex: number,
+ offset: number,
+ usage: BufferUsage,
+ bindGroupVisibility: 'compute' | 'fragment',
+ renderPassEncoder: GPURenderPassEncoder,
+ usedBindGroupLayouts: GPUBindGroupLayout[]
+ ) => {
+ switch (usage) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup = t.createBindGroupForTest(buffer, offset, usage, bindGroupVisibility);
+ renderPassEncoder.setBindGroup(bufferIndex, bindGroup);
+ // To "use" the bind group we will set the corresponding bind group layout in the
+ // pipeline layout when creating the render pipeline.
+ if (bufferAccessibleInDraw && bindGroupVisibility === 'fragment') {
+ usedBindGroupLayouts.push(t.createBindGroupLayoutForTest(usage, bindGroupVisibility));
+ }
+ break;
+ }
+ case 'vertex': {
+ renderPassEncoder.setVertexBuffer(bufferIndex, buffer, offset);
+ break;
+ }
+ case 'index': {
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16', offset);
+ break;
+ }
+ case 'indirect':
+ case 'indexedIndirect': {
+ // We will handle the indirect draw calls later.
+ break;
+ }
+ }
+ };
+
+ const MakeDrawCallWithOneUsage = (
+ usage: BufferUsage,
+ offset: number,
+ renderPassEncoder: GPURenderPassEncoder
+ ) => {
+ switch (usage) {
+ case 'uniform':
+ case 'read-only-storage':
+ case 'storage':
+ case 'vertex':
+ renderPassEncoder.draw(1);
+ break;
+ case 'index':
+ renderPassEncoder.drawIndexed(1);
+ break;
+ case 'indirect':
+ renderPassEncoder.drawIndirect(buffer, offset);
+ break;
+ case 'indexedIndirect': {
+ const indexBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ renderPassEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ renderPassEncoder.drawIndexedIndirect(buffer, offset);
+ break;
+ }
+ }
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+
+ // Set buffer with usage0
+ const offset0 = 0;
+ // Invisible bind groups or vertex buffers are all bound to the slot 1.
+ const bufferIndex0 = visibility0 === 'fragment' ? 0 : 1;
+ const usedBindGroupLayouts: GPUBindGroupLayout[] = [];
+
+ UseBufferOnRenderPassEncoder(
+ usage0AccessibleInDraw,
+ bufferIndex0,
+ offset0,
+ usage0,
+ visibility0,
+ renderPassEncoder,
+ usedBindGroupLayouts
+ );
+
+ let vertexBufferCount = 0;
+
+ // Set pipeline and do draw call if drawBeforeUsage1 === true
+ if (drawBeforeUsage1) {
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: usedBindGroupLayouts,
+ });
+ // To "use" the vertex buffer we need to set the corresponding vertex buffer layout when
+ // creating the render pipeline.
+ if (usage0 === 'vertex' && usage0AccessibleInDraw) {
+ ++vertexBufferCount;
+ }
+ const pipeline = t.createRenderPipelineForTest(pipelineLayout, vertexBufferCount);
+ renderPassEncoder.setPipeline(pipeline);
+ if (!usage0AccessibleInDraw) {
+ renderPassEncoder.draw(1);
+ } else {
+ MakeDrawCallWithOneUsage(usage0, offset0, renderPassEncoder);
+ }
+ }
+
+ // Set buffer with usage1.
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ let bufferIndex1 = 0;
+ if (visibility1 !== 'fragment') {
+ // Invisible bind groups or vertex buffers are all bound to the slot 1.
+ bufferIndex1 = 1;
+ } else if (visibility0 === 'fragment' && usage0AccessibleInDraw) {
+ // When buffer is bound to different bind groups or bound as vertex buffers in one render pass
+ // encoder, the second buffer binding should consume the slot 1.
+ if (IsBufferUsageInBindGroup(usage0) && IsBufferUsageInBindGroup(usage1)) {
+ bufferIndex1 = 1;
+ } else if (usage0 === 'vertex' && usage1 === 'vertex') {
+ bufferIndex1 = 1;
+ }
+ }
+
+ UseBufferOnRenderPassEncoder(
+ usage1AccessibleInDraw,
+ bufferIndex1,
+ offset1,
+ usage1,
+ visibility1,
+ renderPassEncoder,
+ usedBindGroupLayouts
+ );
+
+ // Set pipeline and do draw call if drawBeforeUsage1 === false
+ if (!drawBeforeUsage1) {
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: usedBindGroupLayouts,
+ });
+ if (usage1 === 'vertex' && usage1AccessibleInDraw) {
+ // To "use" the vertex buffer we need to set the corresponding vertex buffer layout when
+ // creating the render pipeline.
+ ++vertexBufferCount;
+ }
+ const pipeline = t.createRenderPipelineForTest(pipelineLayout, vertexBufferCount);
+ renderPassEncoder.setPipeline(pipeline);
+
+ assert(usage0 !== 'indirect');
+ if (!usage0AccessibleInDraw && !usage1AccessibleInDraw) {
+ renderPassEncoder.draw(1);
+ } else if (usage0AccessibleInDraw && !usage1AccessibleInDraw) {
+ MakeDrawCallWithOneUsage(usage0, offset0, renderPassEncoder);
+ } else if (!usage0AccessibleInDraw && usage1AccessibleInDraw) {
+ MakeDrawCallWithOneUsage(usage1, offset1, renderPassEncoder);
+ } else {
+ if (usage1 === 'indexedIndirect') {
+ // If the index buffer has already been set (as usage0), we won't need to set another
+ // index buffer.
+ if (usage0 !== 'index') {
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ renderPassEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ }
+ renderPassEncoder.drawIndexedIndirect(buffer, offset1);
+ } else if (usage1 === 'indirect') {
+ assert(usage0 !== 'index');
+ renderPassEncoder.drawIndirect(buffer, offset1);
+ } else if (usage0 === 'index' || usage1 === 'index') {
+ // We need to call drawIndexed to "use" the index buffer (as usage0 or usage1).
+ renderPassEncoder.drawIndexed(1);
+ } else {
+ renderPassEncoder.draw(1);
+ }
+ }
+ }
+ renderPassEncoder.end();
+
+ const fail = (usage0 === 'storage') !== (usage1 === 'storage');
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, fail);
+ });
+
+g.test('subresources,buffer_usage_in_one_render_pass_with_two_draws')
+ .desc(
+ `
+Test that when one buffer is used in different draw calls in one render pass, its list of internal
+usages within one usage scope (all the commands in the whole render pass) can only be a compatible
+usage list, and the usage scope rules are not related to the buffer offset, while the draw calls in
+different render pass encoders belong to different usage scopes.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', kAllBufferUsages)
+ .combine('usage1', kAllBufferUsages)
+ .beginSubcases()
+ .combine('inSamePass', [true, false])
+ .combine('hasOverlap', [true, false])
+ )
+ .fn(async t => {
+ const { usage0, usage1, inSamePass, hasOverlap } = t.params;
+ const buffer = t.createBufferWithState('valid', {
+ size: kBoundBufferSize * 2,
+ usage:
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX |
+ GPUBufferUsage.INDIRECT,
+ });
+ const UseBufferOnRenderPassEncoderInDrawCall = (
+ offset: number,
+ usage: BufferUsage,
+ renderPassEncoder: GPURenderPassEncoder
+ ) => {
+ switch (usage) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroupLayout = t.createBindGroupLayoutForTest(usage, 'fragment');
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ });
+ const pipeline = t.createRenderPipelineForTest(pipelineLayout, 0);
+ renderPassEncoder.setPipeline(pipeline);
+ const bindGroup = t.createBindGroupForTest(buffer, offset, usage, 'fragment');
+ renderPassEncoder.setBindGroup(0, bindGroup);
+ renderPassEncoder.draw(1);
+ break;
+ }
+ case 'vertex': {
+ const kVertexBufferCount = 1;
+ const pipeline = t.createRenderPipelineForTest('auto', kVertexBufferCount);
+ renderPassEncoder.setPipeline(pipeline);
+ renderPassEncoder.setVertexBuffer(0, buffer, offset);
+ renderPassEncoder.draw(1);
+ break;
+ }
+ case 'index': {
+ const pipeline = t.createRenderPipelineForTest('auto', 0);
+ renderPassEncoder.setPipeline(pipeline);
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16', offset);
+ renderPassEncoder.drawIndexed(1);
+ break;
+ }
+ case 'indirect': {
+ const pipeline = t.createRenderPipelineForTest('auto', 0);
+ renderPassEncoder.setPipeline(pipeline);
+ renderPassEncoder.drawIndirect(buffer, offset);
+ break;
+ }
+ case 'indexedIndirect': {
+ const pipeline = t.createRenderPipelineForTest('auto', 0);
+ renderPassEncoder.setPipeline(pipeline);
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ renderPassEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ renderPassEncoder.drawIndexedIndirect(buffer, offset);
+ break;
+ }
+ }
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+
+ const offset0 = 0;
+ UseBufferOnRenderPassEncoderInDrawCall(offset0, usage0, renderPassEncoder);
+
+ const offset1 = hasOverlap ? offset0 : kBoundBufferSize;
+ if (inSamePass) {
+ UseBufferOnRenderPassEncoderInDrawCall(offset1, usage1, renderPassEncoder);
+ renderPassEncoder.end();
+ } else {
+ renderPassEncoder.end();
+ const anotherRenderPassEncoder = t.beginSimpleRenderPass(encoder);
+ UseBufferOnRenderPassEncoderInDrawCall(offset1, usage1, anotherRenderPassEncoder);
+ anotherRenderPassEncoder.end();
+ }
+
+ const fail = inSamePass && (usage0 === 'storage') !== (usage1 === 'storage');
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, fail);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_misc.spec.ts
new file mode 100644
index 0000000000..cc82cdc73f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/buffer/in_pass_misc.spec.ts
@@ -0,0 +1,409 @@
+export const description = `
+Test other buffer usage validation rules that are not tests in ./in_pass_encoder.spec.js.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../../common/util/util.js';
+
+import { BufferUsage, BufferResourceUsageTest, kAllBufferUsages } from './in_pass_encoder.spec.js';
+
+export const g = makeTestGroup(BufferResourceUsageTest);
+
+const kBufferSize = 256;
+
+g.test('subresources,reset_buffer_usage_before_dispatch')
+ .desc(
+ `
+Test that the buffer usages which are reset by another state-setting commands before a dispatch call
+do not contribute directly to any usage scope in a compute pass.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage'] as const)
+ .combine('usage1', ['uniform', 'storage', 'read-only-storage', 'indirect'] as const)
+ )
+ .fn(async t => {
+ const { usage0, usage1 } = t.params;
+
+ const kUsages = GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT;
+ const buffer = t.createBufferWithState('valid', {
+ size: kBufferSize,
+ usage: kUsages,
+ });
+ const anotherBuffer = t.createBufferWithState('valid', {
+ size: kBufferSize,
+ usage: kUsages,
+ });
+
+ const bindGroupLayouts: GPUBindGroupLayout[] = [
+ t.createBindGroupLayoutForTest(usage0, 'compute'),
+ ];
+ if (usage1 !== 'indirect') {
+ bindGroupLayouts.push(t.createBindGroupLayoutForTest(usage1, 'compute'));
+ }
+ const pipelineLayout = t.device.createPipelineLayout({ bindGroupLayouts });
+ const computePipeline = t.createNoOpComputePipeline(pipelineLayout);
+
+ const encoder = t.device.createCommandEncoder();
+ const computePassEncoder = encoder.beginComputePass();
+ computePassEncoder.setPipeline(computePipeline);
+
+ // Set usage0 for buffer at bind group index 0
+ const bindGroup0 = t.createBindGroupForTest(buffer, 0, usage0, 'compute');
+ computePassEncoder.setBindGroup(0, bindGroup0);
+
+ // Reset bind group index 0 with another bind group that uses anotherBuffer
+ const anotherBindGroup = t.createBindGroupForTest(anotherBuffer, 0, usage0, 'compute');
+ computePassEncoder.setBindGroup(0, anotherBindGroup);
+
+ // Set usage1 for buffer
+ switch (usage1) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup1 = t.createBindGroupForTest(buffer, 0, usage1, 'compute');
+ computePassEncoder.setBindGroup(1, bindGroup1);
+ computePassEncoder.dispatchWorkgroups(1);
+ break;
+ }
+ case 'indirect': {
+ computePassEncoder.dispatchWorkgroupsIndirect(buffer, 0);
+ break;
+ }
+ }
+ computePassEncoder.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
+
+g.test('subresources,reset_buffer_usage_before_draw')
+ .desc(
+ `
+Test that the buffer usages which are reset by another state-setting commands before a draw call
+still contribute directly to the usage scope of the draw call.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', ['uniform', 'storage', 'read-only-storage', 'vertex', 'index'] as const)
+ .combine('usage1', kAllBufferUsages)
+ .unless(t => {
+ return t.usage0 === 'index' && t.usage1 === 'indirect';
+ })
+ )
+ .fn(async t => {
+ const { usage0, usage1 } = t.params;
+
+ const kUsages =
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.INDIRECT |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX;
+ const buffer = t.createBufferWithState('valid', {
+ size: kBufferSize,
+ usage: kUsages,
+ });
+ const anotherBuffer = t.createBufferWithState('valid', {
+ size: kBufferSize,
+ usage: kUsages,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+
+ const bindGroupLayouts: GPUBindGroupLayout[] = [];
+ let vertexBufferCount = 0;
+
+ // Set buffer as usage0 and reset buffer with anotherBuffer as usage0
+ switch (usage0) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup0 = t.createBindGroupForTest(buffer, 0, usage0, 'fragment');
+ renderPassEncoder.setBindGroup(bindGroupLayouts.length, bindGroup0);
+
+ const anotherBindGroup = t.createBindGroupForTest(anotherBuffer, 0, usage0, 'fragment');
+ renderPassEncoder.setBindGroup(bindGroupLayouts.length, anotherBindGroup);
+
+ bindGroupLayouts.push(t.createBindGroupLayoutForTest(usage0, 'fragment'));
+ break;
+ }
+ case 'vertex': {
+ renderPassEncoder.setVertexBuffer(vertexBufferCount, buffer);
+ renderPassEncoder.setVertexBuffer(vertexBufferCount, anotherBuffer);
+
+ ++vertexBufferCount;
+ break;
+ }
+ case 'index': {
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16');
+ renderPassEncoder.setIndexBuffer(anotherBuffer, 'uint16');
+ break;
+ }
+ }
+
+ // Set buffer as usage1
+ switch (usage1) {
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup1 = t.createBindGroupForTest(buffer, 0, usage1, 'fragment');
+ renderPassEncoder.setBindGroup(bindGroupLayouts.length, bindGroup1);
+
+ bindGroupLayouts.push(t.createBindGroupLayoutForTest(usage1, 'fragment'));
+ break;
+ }
+ case 'vertex': {
+ renderPassEncoder.setVertexBuffer(vertexBufferCount, buffer);
+ ++vertexBufferCount;
+ break;
+ }
+ case 'index': {
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16');
+ break;
+ }
+ case 'indirect':
+ case 'indexedIndirect':
+ break;
+ }
+
+ // Add draw call
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts,
+ });
+ const renderPipeline = t.createRenderPipelineForTest(pipelineLayout, vertexBufferCount);
+ renderPassEncoder.setPipeline(renderPipeline);
+ switch (usage1) {
+ case 'indexedIndirect': {
+ if (usage0 !== 'index') {
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ renderPassEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ }
+ renderPassEncoder.drawIndexedIndirect(buffer, 0);
+ break;
+ }
+ case 'indirect': {
+ renderPassEncoder.drawIndirect(buffer, 0);
+ break;
+ }
+ case 'index': {
+ renderPassEncoder.drawIndexed(1);
+ break;
+ }
+ case 'vertex':
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ if (usage0 === 'index') {
+ renderPassEncoder.drawIndexed(1);
+ } else {
+ renderPassEncoder.draw(1);
+ }
+ break;
+ }
+ }
+
+ renderPassEncoder.end();
+
+ const fail = (usage0 === 'storage') !== (usage1 === 'storage');
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, fail);
+ });
+
+g.test('subresources,buffer_usages_in_copy_and_pass')
+ .desc(
+ `
+ Test that using one buffer in a copy command, a render or compute pass encoder is always allowed
+ as WebGPU SPEC (chapter 3.4.5) defines that out of any pass encoder, each command belongs to one
+ separated usage scope.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', [
+ 'copy-src',
+ 'copy-dst',
+ 'uniform',
+ 'storage',
+ 'read-only-storage',
+ 'vertex',
+ 'index',
+ 'indirect',
+ 'indexedIndirect',
+ ] as const)
+ .combine('usage1', [
+ 'copy-src',
+ 'copy-dst',
+ 'uniform',
+ 'storage',
+ 'read-only-storage',
+ 'vertex',
+ 'index',
+ 'indirect',
+ 'indexedIndirect',
+ ] as const)
+ .combine('pass', ['render', 'compute'])
+ .unless(({ usage0, usage1, pass }) => {
+ const IsCopy = (usage: BufferUsage | 'copy-src' | 'copy-dst') => {
+ return usage === 'copy-src' || usage === 'copy-dst';
+ };
+ // We intend to test copy usages in this test.
+ if (!IsCopy(usage0) && !IsCopy(usage1)) {
+ return true;
+ }
+ // When both usage0 and usage1 are copy usages, 'pass' is meaningless so in such situation
+ // we just need to reserve one value as 'pass'.
+ if (IsCopy(usage0) && IsCopy(usage1)) {
+ return pass === 'compute';
+ }
+
+ const IsValidComputeUsage = (usage: BufferUsage | 'copy-src' | 'copy-dst') => {
+ switch (usage) {
+ case 'vertex':
+ case 'index':
+ case 'indexedIndirect':
+ return false;
+ default:
+ return true;
+ }
+ };
+ if (pass === 'compute') {
+ return !IsValidComputeUsage(usage0) || !IsValidComputeUsage(usage1);
+ }
+
+ return false;
+ })
+ )
+ .fn(async t => {
+ const { usage0, usage1, pass } = t.params;
+
+ const kUsages =
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.INDIRECT |
+ GPUBufferUsage.VERTEX |
+ GPUBufferUsage.INDEX;
+ const buffer = t.createBufferWithState('valid', {
+ size: kBufferSize,
+ usage: kUsages,
+ });
+
+ const UseBufferOnCommandEncoder = (
+ usage:
+ | 'copy-src'
+ | 'copy-dst'
+ | 'uniform'
+ | 'storage'
+ | 'read-only-storage'
+ | 'vertex'
+ | 'index'
+ | 'indirect'
+ | 'indexedIndirect',
+ encoder: GPUCommandEncoder
+ ) => {
+ switch (usage) {
+ case 'copy-src': {
+ const destinationBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ encoder.copyBufferToBuffer(buffer, 0, destinationBuffer, 0, 4);
+ break;
+ }
+ case 'copy-dst': {
+ const sourceBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ encoder.copyBufferToBuffer(sourceBuffer, 0, buffer, 0, 4);
+ break;
+ }
+ case 'uniform':
+ case 'storage':
+ case 'read-only-storage': {
+ const bindGroup = t.createBindGroupForTest(buffer, 0, usage, 'fragment');
+ switch (pass) {
+ case 'render': {
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ renderPassEncoder.setBindGroup(0, bindGroup);
+ renderPassEncoder.end();
+ break;
+ }
+ case 'compute': {
+ const computePassEncoder = encoder.beginComputePass();
+ computePassEncoder.setBindGroup(0, bindGroup);
+ computePassEncoder.end();
+ break;
+ }
+ default:
+ unreachable();
+ }
+ break;
+ }
+ case 'vertex': {
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ renderPassEncoder.setVertexBuffer(0, buffer);
+ renderPassEncoder.end();
+ break;
+ }
+ case 'index': {
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ renderPassEncoder.setIndexBuffer(buffer, 'uint16');
+ renderPassEncoder.end();
+ break;
+ }
+ case 'indirect': {
+ switch (pass) {
+ case 'render': {
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ const renderPipeline = t.createNoOpRenderPipeline();
+ renderPassEncoder.setPipeline(renderPipeline);
+ renderPassEncoder.drawIndirect(buffer, 0);
+ renderPassEncoder.end();
+ break;
+ }
+ case 'compute': {
+ const computePassEncoder = encoder.beginComputePass();
+ const computePipeline = t.createNoOpComputePipeline();
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroupsIndirect(buffer, 0);
+ computePassEncoder.end();
+ break;
+ }
+ default:
+ unreachable();
+ }
+ break;
+ }
+ case 'indexedIndirect': {
+ const renderPassEncoder = t.beginSimpleRenderPass(encoder);
+ const renderPipeline = t.createNoOpRenderPipeline();
+ renderPassEncoder.setPipeline(renderPipeline);
+ const indexBuffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.INDEX,
+ });
+ renderPassEncoder.setIndexBuffer(indexBuffer, 'uint16');
+ renderPassEncoder.drawIndexedIndirect(buffer, 0);
+ renderPassEncoder.end();
+ break;
+ }
+ default:
+ unreachable();
+ }
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ UseBufferOnCommandEncoder(usage0, encoder);
+ UseBufferOnCommandEncoder(usage1, encoder);
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts
new file mode 100644
index 0000000000..26a2a66db6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts
@@ -0,0 +1,1376 @@
+export const description = `
+Texture Usages Validation Tests in Render Pass and Compute Pass.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { pp } from '../../../../../common/util/preprocessor.js';
+import { assert } from '../../../../../common/util/util.js';
+import {
+ kDepthStencilFormats,
+ kDepthStencilFormatResolvedAspect,
+ kTextureFormatInfo,
+} from '../../../../capability_info.js';
+import { GPUConst } from '../../../../constants.js';
+import { ValidationTest } from '../../validation_test.js';
+
+type TextureBindingType = 'sampled-texture' | 'multisampled-texture' | 'writeonly-storage-texture';
+const kTextureBindingTypes = [
+ 'sampled-texture',
+ 'multisampled-texture',
+ 'writeonly-storage-texture',
+] as const;
+
+const SIZE = 32;
+class TextureUsageTracking extends ValidationTest {
+ createTexture(
+ options: {
+ width?: number;
+ height?: number;
+ arrayLayerCount?: number;
+ mipLevelCount?: number;
+ sampleCount?: number;
+ format?: GPUTextureFormat;
+ usage?: GPUTextureUsageFlags;
+ } = {}
+ ): GPUTexture {
+ const {
+ width = SIZE,
+ height = SIZE,
+ arrayLayerCount = 1,
+ mipLevelCount = 1,
+ sampleCount = 1,
+ format = 'rgba8unorm',
+ usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ } = options;
+
+ return this.device.createTexture({
+ size: { width, height, depthOrArrayLayers: arrayLayerCount },
+ mipLevelCount,
+ sampleCount,
+ dimension: '2d',
+ format,
+ usage,
+ });
+ }
+
+ createBindGroupLayout(
+ binding: number,
+ bindingType: TextureBindingType,
+ viewDimension: GPUTextureViewDimension,
+ options: {
+ format?: GPUTextureFormat;
+ sampleType?: GPUTextureSampleType;
+ } = {}
+ ): GPUBindGroupLayout {
+ const { sampleType, format } = options;
+ let entry: Omit<GPUBindGroupLayoutEntry, 'binding' | 'visibility'>;
+ switch (bindingType) {
+ case 'sampled-texture':
+ entry = { texture: { viewDimension, sampleType } };
+ break;
+ case 'multisampled-texture':
+ entry = { texture: { viewDimension, multisampled: true, sampleType } };
+ break;
+ case 'writeonly-storage-texture':
+ assert(format !== undefined);
+ entry = { storageTexture: { access: 'write-only', format, viewDimension } };
+ break;
+ }
+
+ return this.device.createBindGroupLayout({
+ entries: [
+ { binding, visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, ...entry },
+ ],
+ });
+ }
+
+ createBindGroup(
+ binding: number,
+ resource: GPUTextureView,
+ bindingType: TextureBindingType,
+ viewDimension: GPUTextureViewDimension,
+ options: {
+ format?: GPUTextureFormat;
+ sampleType?: GPUTextureSampleType;
+ } = {}
+ ): GPUBindGroup {
+ return this.device.createBindGroup({
+ entries: [{ binding, resource }],
+ layout: this.createBindGroupLayout(binding, bindingType, viewDimension, options),
+ });
+ }
+
+ createAndExecuteBundle(
+ binding: number,
+ bindGroup: GPUBindGroup,
+ pass: GPURenderPassEncoder,
+ depthStencilFormat?: GPUTextureFormat
+ ) {
+ const bundleEncoder = this.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ depthStencilFormat,
+ });
+ bundleEncoder.setBindGroup(binding, bindGroup);
+ const bundle = bundleEncoder.finish();
+ pass.executeBundles([bundle]);
+ }
+
+ beginSimpleRenderPass(encoder: GPUCommandEncoder, view: GPUTextureView): GPURenderPassEncoder {
+ return encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ }
+
+ /**
+ * Create two bind groups. Resource usages conflict between these two bind groups. But resource
+ * usage inside each bind group doesn't conflict.
+ */
+ makeConflictingBindGroups() {
+ const view = this.createTexture({
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
+ }).createView();
+ const bindGroupLayouts = [
+ this.createBindGroupLayout(0, 'sampled-texture', '2d'),
+ this.createBindGroupLayout(0, 'writeonly-storage-texture', '2d', { format: 'rgba8unorm' }),
+ ];
+ return {
+ bindGroupLayouts,
+ bindGroups: [
+ this.device.createBindGroup({
+ layout: bindGroupLayouts[0],
+ entries: [{ binding: 0, resource: view }],
+ }),
+ this.device.createBindGroup({
+ layout: bindGroupLayouts[1],
+ entries: [{ binding: 0, resource: view }],
+ }),
+ ],
+ };
+ }
+
+ testValidationScope(
+ compute: boolean
+ ): {
+ bindGroup0: GPUBindGroup;
+ bindGroup1: GPUBindGroup;
+ encoder: GPUCommandEncoder;
+ pass: GPURenderPassEncoder | GPUComputePassEncoder;
+ pipeline: GPURenderPipeline | GPUComputePipeline;
+ } {
+ const { bindGroupLayouts, bindGroups } = this.makeConflictingBindGroups();
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = compute
+ ? encoder.beginComputePass()
+ : this.beginSimpleRenderPass(encoder, this.createTexture().createView());
+
+ // Create pipeline. Note that bindings unused in pipeline should be validated too.
+ const pipelineLayout = this.device.createPipelineLayout({
+ bindGroupLayouts,
+ });
+ const pipeline = compute
+ ? this.createNoOpComputePipeline(pipelineLayout)
+ : this.createNoOpRenderPipeline(pipelineLayout);
+ return {
+ bindGroup0: bindGroups[0],
+ bindGroup1: bindGroups[1],
+ encoder,
+ pass,
+ pipeline,
+ };
+ }
+
+ setPipeline(
+ pass: GPURenderPassEncoder | GPUComputePassEncoder,
+ pipeline: GPURenderPipeline | GPUComputePipeline
+ ) {
+ if (pass instanceof GPUComputePassEncoder) {
+ pass.setPipeline(pipeline as GPUComputePipeline);
+ } else {
+ pass.setPipeline(pipeline as GPURenderPipeline);
+ }
+ }
+
+ issueDrawOrDispatch(pass: GPURenderPassEncoder | GPUComputePassEncoder) {
+ if (pass instanceof GPUComputePassEncoder) {
+ pass.dispatchWorkgroups(1);
+ } else {
+ pass.draw(3, 1, 0, 0);
+ }
+ }
+
+ setComputePipelineAndCallDispatch(pass: GPUComputePassEncoder, layout?: GPUPipelineLayout) {
+ const pipeline = this.createNoOpComputePipeline(layout);
+ pass.setPipeline(pipeline);
+ pass.dispatchWorkgroups(1);
+ }
+}
+
+export const g = makeTestGroup(TextureUsageTracking);
+
+const BASE_LEVEL = 1;
+const TOTAL_LEVELS = 6;
+const BASE_LAYER = 1;
+const TOTAL_LAYERS = 6;
+const SLICE_COUNT = 2;
+
+g.test('subresources_and_binding_types_combination_for_color')
+ .desc(
+ `
+ Test the resource usage rules by using two views of the same GPUTexture in a usage scope. Tests
+ various combinations of {sampled, storage, render target} usages, mip-level ranges, and
+ array-layer ranges, in {compute pass, render pass, render pass via bundle}.
+ - Error if a subresource (level/layer) is used as read+write or write+write in the scope,
+ except when both usages are writeonly-storage-texture which is allowed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('compute', [false, true])
+ .combineWithParams([
+ { _usageOK: true, type0: 'sampled-texture', type1: 'sampled-texture' },
+ { _usageOK: false, type0: 'sampled-texture', type1: 'writeonly-storage-texture' },
+ { _usageOK: false, type0: 'sampled-texture', type1: 'render-target' },
+ // Race condition upon multiple writable storage texture is valid.
+ { _usageOK: true, type0: 'writeonly-storage-texture', type1: 'writeonly-storage-texture' },
+ { _usageOK: false, type0: 'writeonly-storage-texture', type1: 'render-target' },
+ { _usageOK: false, type0: 'render-target', type1: 'render-target' },
+ ] as const)
+ .beginSubcases()
+ .combine('binding0InBundle', [false, true])
+ .combine('binding1InBundle', [false, true])
+ .unless(
+ p =>
+ // We can't set 'render-target' in bundle, so we need to exclude it from bundle.
+ (p.binding0InBundle && p.type0 === 'render-target') ||
+ (p.binding1InBundle && p.type1 === 'render-target') ||
+ // We can't set 'render-target' or bundle in compute.
+ (p.compute &&
+ (p.binding0InBundle ||
+ p.binding1InBundle ||
+ p.type0 === 'render-target' ||
+ p.type1 === 'render-target'))
+ )
+ .combineWithParams([
+ // Two texture usages are binding to the same texture subresource.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL,
+ levelCount1: 1,
+ baseLayer1: BASE_LAYER,
+ layerCount1: 1,
+ _resourceSuccess: false,
+ },
+
+ // Two texture usages are binding to different mip levels of the same texture.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL + 1,
+ levelCount1: 1,
+ baseLayer1: BASE_LAYER,
+ layerCount1: 1,
+ _resourceSuccess: true,
+ },
+
+ // Two texture usages are binding to different array layers of the same texture.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL,
+ levelCount1: 1,
+ baseLayer1: BASE_LAYER + 1,
+ layerCount1: 1,
+ _resourceSuccess: true,
+ },
+
+ // The second texture usage contains the whole mip chain where the first texture usage is
+ // using.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: 0,
+ levelCount1: TOTAL_LEVELS,
+ baseLayer1: BASE_LAYER,
+ layerCount1: 1,
+ _resourceSuccess: false,
+ },
+
+ // The second texture usage contains all layers where the first texture usage is using.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL,
+ levelCount1: 1,
+ baseLayer1: 0,
+ layerCount1: TOTAL_LAYERS,
+ _resourceSuccess: false,
+ },
+
+ // The second texture usage contains all subresources where the first texture usage is
+ // using.
+ {
+ levelCount0: 1,
+ layerCount0: 1,
+ baseLevel1: 0,
+ levelCount1: TOTAL_LEVELS,
+ baseLayer1: 0,
+ layerCount1: TOTAL_LAYERS,
+ _resourceSuccess: false,
+ },
+
+ // Both of the two usages access a few mip levels on the same layer but they don't overlap.
+ {
+ levelCount0: SLICE_COUNT,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL + SLICE_COUNT,
+ levelCount1: 3,
+ baseLayer1: BASE_LAYER,
+ layerCount1: 1,
+ _resourceSuccess: true,
+ },
+
+ // Both of the two usages access a few mip levels on the same layer and they overlap.
+ {
+ levelCount0: SLICE_COUNT,
+ layerCount0: 1,
+ baseLevel1: BASE_LEVEL + SLICE_COUNT - 1,
+ levelCount1: 3,
+ baseLayer1: BASE_LAYER,
+ layerCount1: 1,
+ _resourceSuccess: false,
+ },
+
+ // Both of the two usages access a few array layers on the same level but they don't
+ // overlap.
+ {
+ levelCount0: 1,
+ layerCount0: SLICE_COUNT,
+ baseLevel1: BASE_LEVEL,
+ levelCount1: 1,
+ baseLayer1: BASE_LAYER + SLICE_COUNT,
+ layerCount1: 3,
+ _resourceSuccess: true,
+ },
+
+ // Both of the two usages access a few array layers on the same level and they overlap.
+ {
+ levelCount0: 1,
+ layerCount0: SLICE_COUNT,
+ baseLevel1: BASE_LEVEL,
+ levelCount1: 1,
+ baseLayer1: BASE_LAYER + SLICE_COUNT - 1,
+ layerCount1: 3,
+ _resourceSuccess: false,
+ },
+
+ // Both of the two usages access a few array layers and mip levels but they don't overlap.
+ {
+ levelCount0: SLICE_COUNT,
+ layerCount0: SLICE_COUNT,
+ baseLevel1: BASE_LEVEL + SLICE_COUNT,
+ levelCount1: 3,
+ baseLayer1: BASE_LAYER + SLICE_COUNT,
+ layerCount1: 3,
+ _resourceSuccess: true,
+ },
+
+ // Both of the two usages access a few array layers and mip levels and they overlap.
+ {
+ levelCount0: SLICE_COUNT,
+ layerCount0: SLICE_COUNT,
+ baseLevel1: BASE_LEVEL + SLICE_COUNT - 1,
+ levelCount1: 3,
+ baseLayer1: BASE_LAYER + SLICE_COUNT - 1,
+ layerCount1: 3,
+ _resourceSuccess: false,
+ },
+ ])
+ .unless(
+ p =>
+ // Every color attachment or storage texture can use only one single subresource.
+ (p.type0 !== 'sampled-texture' && (p.levelCount0 !== 1 || p.layerCount0 !== 1)) ||
+ (p.type1 !== 'sampled-texture' && (p.levelCount1 !== 1 || p.layerCount1 !== 1)) ||
+ // All color attachments' size should be the same.
+ (p.type0 === 'render-target' &&
+ p.type1 === 'render-target' &&
+ p.baseLevel1 !== BASE_LEVEL)
+ )
+ )
+ .fn(t => {
+ const {
+ compute,
+ binding0InBundle,
+ binding1InBundle,
+ levelCount0,
+ layerCount0,
+ baseLevel1,
+ baseLayer1,
+ levelCount1,
+ layerCount1,
+ type0,
+ type1,
+ _usageOK,
+ _resourceSuccess,
+ } = t.params;
+
+ const texture = t.createTexture({
+ arrayLayerCount: TOTAL_LAYERS,
+ mipLevelCount: TOTAL_LEVELS,
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.STORAGE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const dimension0 = layerCount0 !== 1 ? '2d-array' : '2d';
+ const view0 = texture.createView({
+ dimension: dimension0,
+ baseMipLevel: BASE_LEVEL,
+ mipLevelCount: levelCount0,
+ baseArrayLayer: BASE_LAYER,
+ arrayLayerCount: layerCount0,
+ });
+
+ const dimension1 = layerCount1 !== 1 ? '2d-array' : '2d';
+ const view1 = texture.createView({
+ dimension: dimension1,
+ baseMipLevel: baseLevel1,
+ mipLevelCount: levelCount1,
+ baseArrayLayer: baseLayer1,
+ arrayLayerCount: layerCount1,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ if (type0 === 'render-target') {
+ // Note that type1 is 'render-target' too. So we don't need to create bindings.
+ assert(type1 === 'render-target');
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: view0,
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ {
+ view: view1,
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ } else {
+ const pass = compute
+ ? encoder.beginComputePass()
+ : t.beginSimpleRenderPass(
+ encoder,
+ type1 === 'render-target' ? view1 : t.createTexture().createView()
+ );
+
+ const bgls: GPUBindGroupLayout[] = [];
+ // Create bind groups. Set bind groups in pass directly or set bind groups in bundle.
+ const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'rgba8unorm';
+
+ const bgl0 = t.createBindGroupLayout(0, type0, dimension0, { format: storageTextureFormat0 });
+ const bindGroup0 = t.device.createBindGroup({
+ layout: bgl0,
+ entries: [{ binding: 0, resource: view0 }],
+ });
+ bgls.push(bgl0);
+
+ if (binding0InBundle) {
+ assert(pass instanceof GPURenderPassEncoder);
+ t.createAndExecuteBundle(0, bindGroup0, pass);
+ } else {
+ pass.setBindGroup(0, bindGroup0);
+ }
+ if (type1 !== 'render-target') {
+ const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'rgba8unorm';
+
+ const bgl1 = t.createBindGroupLayout(1, type1, dimension1, {
+ format: storageTextureFormat1,
+ });
+ const bindGroup1 = t.device.createBindGroup({
+ layout: bgl1,
+ entries: [{ binding: 1, resource: view1 }],
+ });
+ bgls.push(bgl1);
+
+ if (binding1InBundle) {
+ assert(pass instanceof GPURenderPassEncoder);
+ t.createAndExecuteBundle(1, bindGroup1, pass);
+ } else {
+ pass.setBindGroup(1, bindGroup1);
+ }
+ }
+ if (compute) {
+ t.setComputePipelineAndCallDispatch(
+ pass as GPUComputePassEncoder,
+ t.device.createPipelineLayout({ bindGroupLayouts: bgls })
+ );
+ }
+ pass.end();
+ }
+
+ const success = _resourceSuccess || _usageOK;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources_and_binding_types_combination_for_aspect')
+ .desc(
+ `
+ Test the resource usage rules by using two views of the same GPUTexture in a usage scope. Tests
+ various combinations of {sampled, render target} usages, {all, depth-only, stencil-only} aspects
+ that overlap a given subresources in {compute pass, render pass, render pass via bundle}.
+ - Error if a subresource (level/layer/aspect) is used as read+write or write+write in the
+ scope.
+ `
+ )
+ .params(u =>
+ u
+ .combine('compute', [false, true])
+ .combine('binding0InBundle', [false, true])
+ .combine('binding1InBundle', [false, true])
+ .combine('format', kDepthStencilFormats)
+ .beginSubcases()
+ .combineWithParams([
+ {
+ baseLevel: BASE_LEVEL,
+ baseLayer: BASE_LAYER,
+ _resourceSuccess: false,
+ },
+ {
+ baseLevel: BASE_LEVEL + 1,
+ baseLayer: BASE_LAYER,
+ _resourceSuccess: true,
+ },
+ {
+ baseLevel: BASE_LEVEL,
+ baseLayer: BASE_LAYER + 1,
+ _resourceSuccess: true,
+ },
+ ])
+ .combine('aspect0', ['all', 'depth-only', 'stencil-only'] as const)
+ .combine('aspect1', ['all', 'depth-only', 'stencil-only'] as const)
+ .unless(
+ p =>
+ (p.aspect0 === 'stencil-only' && !kTextureFormatInfo[p.format].stencil) ||
+ (p.aspect1 === 'stencil-only' && !kTextureFormatInfo[p.format].stencil)
+ )
+ .unless(
+ p =>
+ (p.aspect0 === 'depth-only' && !kTextureFormatInfo[p.format].depth) ||
+ (p.aspect1 === 'depth-only' && !kTextureFormatInfo[p.format].depth)
+ )
+ .combineWithParams([
+ {
+ type0: 'sampled-texture',
+ type1: 'sampled-texture',
+ _usageSuccess: true,
+ },
+ {
+ type0: 'sampled-texture',
+ type1: 'render-target',
+ _usageSuccess: false,
+ },
+ ] as const)
+ .unless(
+ // Can't sample a multiplanar texture without selecting an aspect.
+ p =>
+ kTextureFormatInfo[p.format].depth &&
+ kTextureFormatInfo[p.format].stencil &&
+ ((p.aspect0 === 'all' && p.type0 === 'sampled-texture') ||
+ (p.aspect1 === 'all' && p.type1 === 'sampled-texture'))
+ )
+ .unless(
+ p =>
+ // We can't set 'render-target' in bundle, so we need to exclude it from bundle.
+ p.binding1InBundle && p.type1 === 'render-target'
+ )
+ .unless(
+ p =>
+ // We can't set 'render-target' or bundle in compute. Note that type0 is definitely not
+ // 'render-target'
+ p.compute && (p.binding0InBundle || p.binding1InBundle || p.type1 === 'render-target')
+ )
+ .unless(
+ p =>
+ // Depth-stencil attachment views must encompass all aspects of the texture. Invalid
+ // cases are for depth-stencil textures when the aspect is not 'all'.
+ p.type1 === 'render-target' &&
+ kTextureFormatInfo[p.format].depth &&
+ kTextureFormatInfo[p.format].stencil &&
+ p.aspect1 !== 'all'
+ )
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(t => {
+ const {
+ compute,
+ binding0InBundle,
+ binding1InBundle,
+ format,
+ baseLevel,
+ baseLayer,
+ aspect0,
+ aspect1,
+ type0,
+ type1,
+ _resourceSuccess,
+ _usageSuccess,
+ } = t.params;
+
+ const texture = t.createTexture({
+ arrayLayerCount: TOTAL_LAYERS,
+ mipLevelCount: TOTAL_LEVELS,
+ format,
+ });
+
+ const view0 = texture.createView({
+ dimension: '2d',
+ baseMipLevel: BASE_LEVEL,
+ mipLevelCount: 1,
+ baseArrayLayer: BASE_LAYER,
+ arrayLayerCount: 1,
+ aspect: aspect0,
+ });
+
+ const view1 = texture.createView({
+ dimension: '2d',
+ baseMipLevel: baseLevel,
+ mipLevelCount: 1,
+ baseArrayLayer: baseLayer,
+ arrayLayerCount: 1,
+ aspect: aspect1,
+ });
+ const view1ResolvedFormat = kDepthStencilFormatResolvedAspect[format][aspect1]!;
+ const view1HasDepth = kTextureFormatInfo[view1ResolvedFormat].depth;
+ const view1HasStencil = kTextureFormatInfo[view1ResolvedFormat].stencil;
+
+ const encoder = t.device.createCommandEncoder();
+ // Color attachment's size should match depth/stencil attachment's size. Note that if
+ // type1 !== 'render-target' then there's no depthStencilAttachment to match anyway.
+ const depthStencilFormat = type1 === 'render-target' ? view1ResolvedFormat : undefined;
+
+ const size = SIZE >> baseLevel;
+ const pass = compute
+ ? encoder.beginComputePass()
+ : encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: t.createTexture({ width: size, height: size }).createView(),
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: depthStencilFormat
+ ? {
+ view: view1,
+ depthStoreOp: view1HasDepth ? 'discard' : undefined,
+ depthLoadOp: view1HasDepth ? 'load' : undefined,
+ stencilStoreOp: view1HasStencil ? 'discard' : undefined,
+ stencilLoadOp: view1HasStencil ? 'load' : undefined,
+ }
+ : undefined,
+ });
+
+ const aspectSampleType = (format: GPUTextureFormat, aspect: typeof aspect0) => {
+ switch (aspect) {
+ case 'depth-only':
+ return 'depth';
+ case 'stencil-only':
+ return 'uint';
+ case 'all':
+ assert(kTextureFormatInfo[format].depth !== kTextureFormatInfo[format].stencil);
+ if (kTextureFormatInfo[format].stencil) {
+ return 'uint';
+ }
+ return 'depth';
+ }
+ };
+
+ // Create bind groups. Set bind groups in pass directly or set bind groups in bundle.
+ const bindGroup0 = t.createBindGroup(0, view0, type0, '2d', {
+ sampleType: type0 === 'sampled-texture' ? aspectSampleType(format, aspect0) : undefined,
+ });
+ if (binding0InBundle) {
+ assert(pass instanceof GPURenderPassEncoder);
+ t.createAndExecuteBundle(0, bindGroup0, pass, depthStencilFormat);
+ } else {
+ pass.setBindGroup(0, bindGroup0);
+ }
+ if (type1 !== 'render-target') {
+ const bindGroup1 = t.createBindGroup(1, view1, type1, '2d', {
+ sampleType: type1 === 'sampled-texture' ? aspectSampleType(format, aspect1) : undefined,
+ });
+ if (binding1InBundle) {
+ assert(pass instanceof GPURenderPassEncoder);
+ t.createAndExecuteBundle(1, bindGroup1, pass, depthStencilFormat);
+ } else {
+ pass.setBindGroup(1, bindGroup1);
+ }
+ }
+ if (compute) t.setComputePipelineAndCallDispatch(pass as GPUComputePassEncoder);
+ pass.end();
+
+ const disjointAspects =
+ (aspect0 === 'depth-only' && aspect1 === 'stencil-only') ||
+ (aspect0 === 'stencil-only' && aspect1 === 'depth-only');
+
+ // If subresources' mip/array slices has no overlap, or their binding types don't conflict,
+ // it will definitely success no matter what aspects they are binding to.
+ const success = disjointAspects || _resourceSuccess || _usageSuccess;
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('shader_stages_and_visibility,storage_write')
+ .desc(
+ `
+ Test that stage visibility doesn't affect resource usage validation.
+ - Use a texture as sampled, with 'readVisibility' {0,VERTEX,FRAGMENT,COMPUTE}
+ - Use a {same,different} texture as storage, with 'writeVisibility' {0,FRAGMENT,COMPUTE}
+
+ There should be a validation error IFF the same texture was used.
+ `
+ )
+ .params(u =>
+ u
+ .combine('compute', [false, true])
+ .beginSubcases()
+ .combine('secondUseConflicts', [false, true])
+ .combine('readVisibility', [
+ 0,
+ GPUConst.ShaderStage.VERTEX,
+ GPUConst.ShaderStage.FRAGMENT,
+ GPUConst.ShaderStage.COMPUTE,
+ ])
+ .combine('writeVisibility', [0, GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE])
+ )
+ .fn(t => {
+ const { compute, readVisibility, writeVisibility, secondUseConflicts } = t.params;
+
+ const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING;
+ const view = t.createTexture({ usage }).createView();
+ const view2 = secondUseConflicts ? view : t.createTexture({ usage }).createView();
+
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ { binding: 0, visibility: readVisibility, texture: {} },
+ {
+ binding: 1,
+ visibility: writeVisibility,
+ storageTexture: { access: 'write-only', format: 'rgba8unorm' },
+ },
+ ],
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ { binding: 0, resource: view },
+ { binding: 1, resource: view2 },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ if (compute) {
+ const pass = encoder.beginComputePass();
+ pass.setBindGroup(0, bindGroup);
+
+ t.setComputePipelineAndCallDispatch(
+ pass,
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bgl],
+ })
+ );
+ pass.end();
+ } else {
+ const pass = t.beginSimpleRenderPass(encoder, t.createTexture().createView());
+ pass.setBindGroup(0, bindGroup);
+ pass.end();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, secondUseConflicts);
+ });
+
+g.test('shader_stages_and_visibility,attachment_write')
+ .desc(
+ `
+ Test that stage visibility doesn't affect resource usage validation.
+ - Use a texture as sampled, with 'readVisibility' {0,VERTEX,FRAGMENT,COMPUTE}
+ - Use a {same,different} texture as a render pass attachment
+
+ There should be a validation error IFF the same texture was used.
+ `
+ )
+ .params(u =>
+ u
+ .beginSubcases()
+ .combine('secondUseConflicts', [false, true])
+ .combine('readVisibility', [
+ 0,
+ GPUConst.ShaderStage.VERTEX,
+ GPUConst.ShaderStage.FRAGMENT,
+ GPUConst.ShaderStage.COMPUTE,
+ ])
+ )
+ .fn(t => {
+ const { readVisibility, secondUseConflicts } = t.params;
+
+ // writeonly-storage-texture binding type is not supported in vertex stage. So, this test
+ // uses writeonly-storage-texture binding as writable binding upon the same subresource if
+ // vertex stage is not included. Otherwise, it uses output attachment instead.
+ const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT;
+
+ const view = t.createTexture({ usage }).createView();
+ const view2 = secondUseConflicts ? view : t.createTexture({ usage }).createView();
+ const bgl = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility: readVisibility, texture: {} }],
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: bgl,
+ entries: [{ binding: 0, resource: view }],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = t.beginSimpleRenderPass(encoder, view2);
+ pass.setBindGroup(0, bindGroup);
+ pass.end();
+
+ // Texture usages in bindings with invisible shader stages should be validated. Invisible shader
+ // stages include shader stage with visibility none, compute shader stage in render pass, and
+ // vertex/fragment shader stage in compute pass.
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, secondUseConflicts);
+ });
+
+g.test('replaced_binding')
+ .desc(
+ `
+ Test whether a binding that's been replaced by another setBindGroup call can still
+ cause validation to fail (with a write/write conflict).
+ - In render pass, all setBindGroup calls contribute to the validation even if they're
+ shadowed.
+ - In compute pass, only the bindings visible at dispatchWorkgroups() contribute to validation.
+ `
+ )
+ .params(u =>
+ u
+ .combine('compute', [false, true])
+ .combine('callDrawOrDispatch', [false, true])
+ .combine('entry', [
+ { texture: {} },
+ { storageTexture: { access: 'write-only', format: 'rgba8unorm' } },
+ ] as const)
+ )
+ .fn(t => {
+ const { compute, callDrawOrDispatch, entry } = t.params;
+
+ const sampledView = t.createTexture().createView();
+ const sampledStorageView = t
+ .createTexture({ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING })
+ .createView();
+
+ // Create bindGroup0. It has two bindings. These two bindings use different views/subresources.
+ const bglEntries0: GPUBindGroupLayoutEntry[] = [
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.FRAGMENT,
+ ...entry,
+ },
+ ];
+ const bgEntries0: GPUBindGroupEntry[] = [
+ { binding: 0, resource: sampledView },
+ { binding: 1, resource: sampledStorageView },
+ ];
+ const bindGroup0 = t.device.createBindGroup({
+ entries: bgEntries0,
+ layout: t.device.createBindGroupLayout({ entries: bglEntries0 }),
+ });
+
+ // Create bindGroup1. It has one binding, which use the same view/subresource of a binding in
+ // bindGroup0. So it may or may not conflicts with that binding in bindGroup0.
+ const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', undefined);
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = compute
+ ? encoder.beginComputePass()
+ : t.beginSimpleRenderPass(encoder, t.createTexture().createView());
+
+ // Set bindGroup0 and bindGroup1. bindGroup0 is replaced by bindGroup1 in the current pass.
+ // But bindings in bindGroup0 should be validated too.
+ pass.setBindGroup(0, bindGroup0);
+ if (callDrawOrDispatch) {
+ const pipeline = compute ? t.createNoOpComputePipeline() : t.createNoOpRenderPipeline();
+ t.setPipeline(pass, pipeline);
+ t.issueDrawOrDispatch(pass);
+ }
+ pass.setBindGroup(0, bindGroup1);
+ pass.end();
+
+ // MAINTENANCE_TODO: If the Compatible Usage List
+ // (https://gpuweb.github.io/gpuweb/#compatible-usage-list) gets programmatically defined in
+ // capability_info, use it here, instead of this logic, for clarity.
+ let success = entry.storageTexture?.access !== 'write-only';
+ // Replaced bindings should not be validated in compute pass, because validation only occurs
+ // inside dispatchWorkgroups() which only looks at the current resource usages.
+ success ||= compute;
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('bindings_in_bundle')
+ .desc(
+ `
+ Test the texture usages in bundles by using two bindings of the same texture with various
+ combination of {sampled, storage, render target} usages.
+ `
+ )
+ .params(u =>
+ u
+ .combine('type0', ['render-target', ...kTextureBindingTypes] as const)
+ .combine('type1', ['render-target', ...kTextureBindingTypes] as const)
+ .beginSubcases()
+ .combine('binding0InBundle', [false, true])
+ .combine('binding1InBundle', [false, true])
+ .expandWithParams(function* ({ type0, type1 }) {
+ const usageForType = (type: typeof type0 | typeof type1) => {
+ switch (type) {
+ case 'multisampled-texture':
+ case 'sampled-texture':
+ return 'TEXTURE_BINDING' as const;
+ case 'writeonly-storage-texture':
+ return 'STORAGE_BINDING' as const;
+ case 'render-target':
+ return 'RENDER_ATTACHMENT' as const;
+ }
+ };
+
+ yield {
+ _usage0: usageForType(type0),
+ _usage1: usageForType(type1),
+ _sampleCount:
+ type0 === 'multisampled-texture' || type1 === 'multisampled-texture'
+ ? (4 as const)
+ : undefined,
+ };
+ })
+ .unless(
+ p =>
+ // We can't set 'render-target' in bundle, so we need to exclude it from bundle.
+ // In addition, if both bindings are non-bundle, there is no need to test it because
+ // we have far more comprehensive test cases for that situation in this file.
+ (p.binding0InBundle && p.type0 === 'render-target') ||
+ (p.binding1InBundle && p.type1 === 'render-target') ||
+ (!p.binding0InBundle && !p.binding1InBundle) ||
+ // Storage textures can't be multisampled.
+ (p._sampleCount !== undefined &&
+ p._sampleCount > 1 &&
+ (p._usage0 === 'STORAGE_BINDING' || p._usage1 === 'STORAGE_BINDING')) ||
+ // If both are sampled, we create two views of the same texture, so both must be
+ // multisampled.
+ (p.type0 === 'multisampled-texture' && p.type1 === 'sampled-texture') ||
+ (p.type0 === 'sampled-texture' && p.type1 === 'multisampled-texture')
+ )
+ )
+ .fn(t => {
+ const {
+ binding0InBundle,
+ binding1InBundle,
+ type0,
+ type1,
+ _usage0,
+ _usage1,
+ _sampleCount,
+ } = t.params;
+
+ // Two bindings are attached to the same texture view.
+ const usage =
+ _sampleCount === 4
+ ? GPUTextureUsage[_usage0] | GPUTextureUsage[_usage1] | GPUTextureUsage.RENDER_ATTACHMENT
+ : GPUTextureUsage[_usage0] | GPUTextureUsage[_usage1];
+ const view = t
+ .createTexture({
+ usage,
+ sampleCount: _sampleCount,
+ })
+ .createView();
+
+ const bindGroups: GPUBindGroup[] = [];
+ if (type0 !== 'render-target') {
+ const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm';
+ bindGroups[0] = t.createBindGroup(0, view, type0, '2d', { format: binding0TexFormat });
+ }
+ if (type1 !== 'render-target') {
+ const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm';
+ bindGroups[1] = t.createBindGroup(1, view, type1, '2d', { format: binding1TexFormat });
+ }
+
+ const encoder = t.device.createCommandEncoder();
+ // At least one binding is in bundle, which means that its type is not 'render-target'.
+ // As a result, only one binding's type is 'render-target' at most.
+ const pass = t.beginSimpleRenderPass(
+ encoder,
+ type0 === 'render-target' || type1 === 'render-target' ? view : t.createTexture().createView()
+ );
+
+ const bindingsInBundle: boolean[] = [binding0InBundle, binding1InBundle];
+ for (let i = 0; i < 2; i++) {
+ // Create a bundle for each bind group if its bindings is required to be in bundle on purpose.
+ // Otherwise, call setBindGroup directly in pass if needed (when its binding is not
+ // 'render-target').
+ if (bindingsInBundle[i]) {
+ const bundleEncoder = t.device.createRenderBundleEncoder({
+ colorFormats: ['rgba8unorm'],
+ });
+ bundleEncoder.setBindGroup(i, bindGroups[i]);
+ const bundleInPass = bundleEncoder.finish();
+ pass.executeBundles([bundleInPass]);
+ } else if (bindGroups[i] !== undefined) {
+ pass.setBindGroup(i, bindGroups[i]);
+ }
+ }
+
+ pass.end();
+
+ const isReadOnly = (t: typeof type0 | typeof type1) => {
+ switch (t) {
+ case 'sampled-texture':
+ case 'multisampled-texture':
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ let success = false;
+ if (isReadOnly(type0) && isReadOnly(type1)) {
+ success = true;
+ }
+
+ if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') {
+ success = true;
+ }
+
+ // Resource usages in bundle should be validated.
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('unused_bindings_in_pipeline')
+ .desc(
+ `
+ Test that for compute pipelines with 'auto' layout, only bindings used by the pipeline count
+ toward the usage scope. For render passes, test the pipeline doesn't matter because only the
+ calls to setBindGroup count toward the usage scope.
+ `
+ )
+ .params(u =>
+ u
+ .combine('compute', [false, true])
+ .combine('useBindGroup0', [false, true])
+ .combine('useBindGroup1', [false, true])
+ .combine('setBindGroupsOrder', ['common', 'reversed'] as const)
+ .combine('setPipeline', ['before', 'middle', 'after', 'none'] as const)
+ .combine('callDrawOrDispatch', [false, true])
+ )
+ .fn(t => {
+ const {
+ compute,
+ useBindGroup0,
+ useBindGroup1,
+ setBindGroupsOrder,
+ setPipeline,
+ callDrawOrDispatch,
+ } = t.params;
+ const view = t
+ .createTexture({ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING })
+ .createView();
+ const bindGroup0 = t.createBindGroup(0, view, 'sampled-texture', '2d', {
+ format: 'rgba8unorm',
+ });
+ const bindGroup1 = t.createBindGroup(0, view, 'writeonly-storage-texture', '2d', {
+ format: 'rgba8unorm',
+ });
+
+ const wgslVertex = `@vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+}`;
+ const wgslFragment = pp`
+ ${pp._if(useBindGroup0)}
+ @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>;
+ ${pp._endif}
+ ${pp._if(useBindGroup1)}
+ @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>;
+ ${pp._endif}
+ @fragment fn main() {}
+ `;
+
+ const wgslCompute = pp`
+ ${pp._if(useBindGroup0)}
+ @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>;
+ ${pp._endif}
+ ${pp._if(useBindGroup1)}
+ @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>;
+ ${pp._endif}
+ @compute @workgroup_size(1) fn main() {}
+ `;
+
+ const pipeline = compute
+ ? t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: wgslCompute,
+ }),
+ entryPoint: 'main',
+ },
+ })
+ : t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: wgslVertex,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: wgslFragment,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = compute
+ ? encoder.beginComputePass()
+ : encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: t.createTexture().createView(),
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ const index0 = setBindGroupsOrder === 'common' ? 0 : 1;
+ const index1 = setBindGroupsOrder === 'common' ? 1 : 0;
+ if (setPipeline === 'before') t.setPipeline(pass, pipeline);
+ pass.setBindGroup(index0, bindGroup0);
+ if (setPipeline === 'middle') t.setPipeline(pass, pipeline);
+ pass.setBindGroup(index1, bindGroup1);
+ if (setPipeline === 'after') t.setPipeline(pass, pipeline);
+ if (callDrawOrDispatch) t.issueDrawOrDispatch(pass);
+ pass.end();
+
+ // Resource usage validation scope is defined by the whole render pass or by dispatch calls.
+ // Regardless of whether or not dispatch is called, in a compute pass, we always succeed
+ // because in this test, none of the bindings are used by the pipeline.
+ // In a render pass, we always fail because usage is based on any bindings used in the
+ // render pass, regardless of whether the pipeline uses them.
+ let success = compute;
+
+ // Also fails if we try to draw/dispatch without a pipeline.
+ if (callDrawOrDispatch && setPipeline === 'none') {
+ success = false;
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('scope,dispatch')
+ .desc(
+ `
+ Tests that in a compute pass, no usage validation occurs without a dispatch call.
+ {Sets,skips} each of two conflicting bind groups in a pass {with,without} a dispatch call.
+ If both are set, AND there is a dispatch call, validation should fail.
+ `
+ )
+ .params(u =>
+ u
+ .combine('dispatch', ['none', 'direct', 'indirect'])
+ .beginSubcases()
+ .expand('setBindGroup0', p => (p.dispatch ? [true] : [false, true]))
+ .expand('setBindGroup1', p => (p.dispatch ? [true] : [false, true]))
+ )
+ .fn(t => {
+ const { dispatch, setBindGroup0, setBindGroup1 } = t.params;
+
+ const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(true);
+ assert(pass instanceof GPUComputePassEncoder);
+ t.setPipeline(pass, pipeline);
+
+ if (setBindGroup0) pass.setBindGroup(0, bindGroup0);
+ if (setBindGroup1) pass.setBindGroup(1, bindGroup1);
+
+ switch (dispatch) {
+ case 'direct':
+ pass.dispatchWorkgroups(1);
+ break;
+ case 'indirect':
+ {
+ const indirectBuffer = t.device.createBuffer({ size: 4, usage: GPUBufferUsage.INDIRECT });
+ pass.dispatchWorkgroupsIndirect(indirectBuffer, 0);
+ }
+ break;
+ }
+
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, dispatch !== 'none' && setBindGroup0 && setBindGroup1);
+ });
+
+g.test('scope,basic,render')
+ .desc(
+ `
+ Tests that in a render pass, validation occurs even without a pipeline or draw call.
+ {Set,skip} each of two conflicting bind groups. If both are set, validation should fail.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('setBindGroup0', [false, true])
+ .combine('setBindGroup1', [false, true])
+ )
+ .fn(t => {
+ const { setBindGroup0, setBindGroup1 } = t.params;
+
+ const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false);
+ assert(pass instanceof GPURenderPassEncoder);
+
+ if (setBindGroup0) pass.setBindGroup(0, bindGroup0);
+ if (setBindGroup1) pass.setBindGroup(1, bindGroup1);
+
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, setBindGroup0 && setBindGroup1);
+ });
+
+g.test('scope,pass_boundary,compute')
+ .desc(
+ `
+ Test using two conflicting bind groups in separate dispatch calls, {with,without} a pass
+ boundary in between. This should always be valid.
+ `
+ )
+ .paramsSubcasesOnly(u => u.combine('splitPass', [false, true]))
+ .fn(t => {
+ const { splitPass } = t.params;
+
+ const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups();
+
+ const encoder = t.device.createCommandEncoder();
+
+ const pipelineUsingBG0 = t.createNoOpComputePipeline(
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayouts[0]],
+ })
+ );
+ const pipelineUsingBG1 = t.createNoOpComputePipeline(
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayouts[1]],
+ })
+ );
+
+ let pass = encoder.beginComputePass();
+ pass.setPipeline(pipelineUsingBG0);
+ pass.setBindGroup(0, bindGroups[0]);
+ pass.dispatchWorkgroups(1);
+ if (splitPass) {
+ pass.end();
+ pass = encoder.beginComputePass();
+ }
+ pass.setPipeline(pipelineUsingBG1);
+ pass.setBindGroup(0, bindGroups[1]);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+
+ // Always valid
+ encoder.finish();
+ });
+
+g.test('scope,pass_boundary,render')
+ .desc(
+ `
+ Test using two conflicting bind groups in separate draw calls, {with,without} a pass
+ boundary in between. This should be valid only if there is a pass boundary.
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combine('splitPass', [false, true])
+ .combine('draw', [false, true])
+ )
+ .fn(t => {
+ const { splitPass, draw } = t.params;
+
+ const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups();
+
+ const encoder = t.device.createCommandEncoder();
+
+ const pipelineUsingBG0 = t.createNoOpRenderPipeline(
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayouts[0]],
+ })
+ );
+ const pipelineUsingBG1 = t.createNoOpRenderPipeline(
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayouts[1]],
+ })
+ );
+
+ const attachment = t.createTexture().createView();
+
+ let pass = t.beginSimpleRenderPass(encoder, attachment);
+ pass.setPipeline(pipelineUsingBG0);
+ pass.setBindGroup(0, bindGroups[0]);
+ if (draw) pass.draw(3);
+ if (splitPass) {
+ pass.end();
+ pass = t.beginSimpleRenderPass(encoder, attachment);
+ }
+ pass.setPipeline(pipelineUsingBG1);
+ pass.setBindGroup(0, bindGroups[1]);
+ if (draw) pass.draw(3);
+ pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !splitPass);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts
new file mode 100644
index 0000000000..44c5601e81
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_common.spec.ts
@@ -0,0 +1,572 @@
+export const description = `
+Texture Usages Validation Tests in Same or Different Render Pass Encoders.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../../../common/util/util.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ getColorAttachment(
+ texture: GPUTexture,
+ textureViewDescriptor?: GPUTextureViewDescriptor
+ ): GPURenderPassColorAttachment {
+ const view = texture.createView(textureViewDescriptor);
+
+ return {
+ view,
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ };
+ }
+
+ createBindGroupForTest(
+ textureView: GPUTextureView,
+ textureUsage: 'texture' | 'storage',
+ sampleType: 'float' | 'depth' | 'uint'
+ ) {
+ const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ };
+ switch (textureUsage) {
+ case 'texture':
+ bindGroupLayoutEntry.texture = { viewDimension: '2d-array', sampleType };
+ break;
+ case 'storage':
+ bindGroupLayoutEntry.storageTexture = {
+ access: 'write-only',
+ format: 'rgba8unorm',
+ viewDimension: '2d-array',
+ };
+ break;
+ default:
+ unreachable();
+ break;
+ }
+ const layout = this.device.createBindGroupLayout({
+ entries: [bindGroupLayoutEntry],
+ });
+ return this.device.createBindGroup({
+ layout,
+ entries: [{ binding: 0, resource: textureView }],
+ });
+ }
+
+ isRangeNotOverlapped(start0: number, end0: number, start1: number, end1: number): boolean {
+ assert(start0 <= end0 && start1 <= end1);
+ // There are only two possibilities for two non-overlapped ranges:
+ // [start0, end0] [start1, end1] or
+ // [start1, end1] [start0, end0]
+ return end0 < start1 || end1 < start0;
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kTextureSize = 16;
+const kTextureLevels = 3;
+const kTextureLayers = 3;
+
+g.test('subresources,color_attachments')
+ .desc(
+ `
+ Test that the different subresource of the same texture are allowed to be used as color
+ attachments in same / different render pass encoder, while the same subresource is only allowed
+ to be used as different color attachments in different render pass encoders.`
+ )
+ .params(u =>
+ u
+ .combine('layer0', [0, 1])
+ .combine('level0', [0, 1])
+ .combine('layer1', [0, 1])
+ .combine('level1', [0, 1])
+ .combine('inSamePass', [true, false])
+ .unless(t => t.inSamePass && t.level0 !== t.level1)
+ )
+ .fn(async t => {
+ const { layer0, level0, layer1, level1, inSamePass } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ mipLevelCount: kTextureLevels,
+ });
+
+ const colorAttachment1 = t.getColorAttachment(texture, {
+ dimension: '2d',
+ baseArrayLayer: layer0,
+ arrayLayerCount: 1,
+ baseMipLevel: level0,
+ mipLevelCount: 1,
+ });
+ const colorAttachment2 = t.getColorAttachment(texture, {
+ dimension: '2d',
+ baseArrayLayer: layer1,
+ baseMipLevel: level1,
+ mipLevelCount: 1,
+ });
+ const encoder = t.device.createCommandEncoder();
+ if (inSamePass) {
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment1, colorAttachment2],
+ });
+ renderPass.end();
+ } else {
+ const renderPass1 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment1],
+ });
+ renderPass1.end();
+ const renderPass2 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment2],
+ });
+ renderPass2.end();
+ }
+
+ const success = inSamePass ? layer0 !== layer1 : true;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources,color_attachment_and_bind_group')
+ .desc(
+ `
+ Test that when one subresource of a texture is used as a color attachment, it cannot be used in a
+ bind group simultaneously in the same render pass encoder. It is allowed when the bind group is
+ used in another render pass encoder instead of the same one.`
+ )
+ .params(u =>
+ u
+ .combine('colorAttachmentLevel', [0, 1])
+ .combine('colorAttachmentLayer', [0, 1])
+ .combineWithParams([
+ { bgLevel: 0, bgLevelCount: 1 },
+ { bgLevel: 1, bgLevelCount: 1 },
+ { bgLevel: 1, bgLevelCount: 2 },
+ ])
+ .combineWithParams([
+ { bgLayer: 0, bgLayerCount: 1 },
+ { bgLayer: 1, bgLayerCount: 1 },
+ { bgLayer: 1, bgLayerCount: 2 },
+ ])
+ .combine('bgUsage', ['texture', 'storage'] as const)
+ .unless(t => t.bgUsage === 'storage' && t.bgLevelCount > 1)
+ .combine('inSamePass', [true, false])
+ )
+ .fn(async t => {
+ const {
+ colorAttachmentLevel,
+ colorAttachmentLayer,
+ bgLevel,
+ bgLevelCount,
+ bgLayer,
+ bgLayerCount,
+ bgUsage,
+ inSamePass,
+ } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage:
+ GPUTextureUsage.RENDER_ATTACHMENT |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.STORAGE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ mipLevelCount: kTextureLevels,
+ });
+ const bindGroupView = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: bgLayer,
+ arrayLayerCount: bgLayerCount,
+ baseMipLevel: bgLevel,
+ mipLevelCount: bgLevelCount,
+ });
+ const bindGroup = t.createBindGroupForTest(bindGroupView, bgUsage, 'float');
+
+ const colorAttachment = t.getColorAttachment(texture, {
+ dimension: '2d',
+ baseArrayLayer: colorAttachmentLayer,
+ arrayLayerCount: 1,
+ baseMipLevel: colorAttachmentLevel,
+ mipLevelCount: 1,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment],
+ });
+ if (inSamePass) {
+ renderPass.setBindGroup(0, bindGroup);
+ renderPass.end();
+ } else {
+ renderPass.end();
+
+ const texture2 = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ mipLevelCount: 1,
+ });
+ const colorAttachment2 = t.getColorAttachment(texture2);
+ const renderPass2 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment2],
+ });
+ renderPass2.setBindGroup(0, bindGroup);
+ renderPass2.end();
+ }
+
+ const isMipLevelNotOverlapped = t.isRangeNotOverlapped(
+ colorAttachmentLevel,
+ colorAttachmentLevel,
+ bgLevel,
+ bgLevel + bgLevelCount - 1
+ );
+ const isArrayLayerNotOverlapped = t.isRangeNotOverlapped(
+ colorAttachmentLayer,
+ colorAttachmentLayer,
+ bgLayer,
+ bgLayer + bgLayerCount - 1
+ );
+ const isNotOverlapped = isMipLevelNotOverlapped || isArrayLayerNotOverlapped;
+
+ const success = inSamePass ? isNotOverlapped : true;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources,depth_stencil_attachment_and_bind_group')
+ .desc(
+ `
+ Test that when one subresource of a texture is used as a depth stencil attachment, it cannot be
+ used in a bind group simultaneously in the same render pass encoder. It is allowed when the bind
+ group is used in another render pass encoder instead of the same one, or the subresource is used
+ as a read-only depth stencil attachment.`
+ )
+ .params(u =>
+ u
+ .combine('dsLevel', [0, 1])
+ .combine('dsLayer', [0, 1])
+ .combineWithParams([
+ { bgLevel: 0, bgLevelCount: 1 },
+ { bgLevel: 1, bgLevelCount: 1 },
+ { bgLevel: 1, bgLevelCount: 2 },
+ ])
+ .combineWithParams([
+ { bgLayer: 0, bgLayerCount: 1 },
+ { bgLayer: 1, bgLayerCount: 1 },
+ { bgLayer: 1, bgLayerCount: 2 },
+ ])
+ .combine('dsReadOnly', [true, false])
+ .combine('bgAspect', ['depth-only', 'stencil-only'] as const)
+ .combine('inSamePass', [true, false])
+ )
+ .fn(async t => {
+ const {
+ dsLevel,
+ dsLayer,
+ bgLevel,
+ bgLevelCount,
+ bgLayer,
+ bgLayerCount,
+ dsReadOnly,
+ bgAspect,
+ inSamePass,
+ } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'depth24plus-stencil8',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ mipLevelCount: kTextureLevels,
+ });
+ const bindGroupView = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: bgLayer,
+ arrayLayerCount: bgLayerCount,
+ baseMipLevel: bgLevel,
+ mipLevelCount: bgLevelCount,
+ aspect: bgAspect,
+ });
+ const sampleType = bgAspect === 'depth-only' ? 'depth' : 'uint';
+ const bindGroup = t.createBindGroupForTest(bindGroupView, 'texture', sampleType);
+
+ const attachmentView = texture.createView({
+ dimension: '2d',
+ baseArrayLayer: dsLayer,
+ arrayLayerCount: 1,
+ baseMipLevel: dsLevel,
+ mipLevelCount: 1,
+ });
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: attachmentView,
+ depthReadOnly: dsReadOnly,
+ depthLoadOp: 'load',
+ depthStoreOp: 'store',
+ stencilReadOnly: dsReadOnly,
+ stencilLoadOp: 'load',
+ stencilStoreOp: 'store',
+ };
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment,
+ });
+ if (inSamePass) {
+ renderPass.setBindGroup(0, bindGroup);
+ renderPass.end();
+ } else {
+ renderPass.end();
+
+ const texture2 = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ mipLevelCount: 1,
+ });
+ const colorAttachment2 = t.getColorAttachment(texture2);
+ const renderPass2 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment2],
+ });
+ renderPass2.setBindGroup(0, bindGroup);
+ renderPass2.end();
+ }
+
+ const isMipLevelNotOverlapped = t.isRangeNotOverlapped(
+ dsLevel,
+ dsLevel,
+ bgLevel,
+ bgLevel + bgLevelCount - 1
+ );
+ const isArrayLayerNotOverlapped = t.isRangeNotOverlapped(
+ dsLayer,
+ dsLayer,
+ bgLayer,
+ bgLayer + bgLayerCount - 1
+ );
+ const isNotOverlapped = isMipLevelNotOverlapped || isArrayLayerNotOverlapped;
+
+ const success = !inSamePass || isNotOverlapped || dsReadOnly;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources,multiple_bind_groups')
+ .desc(
+ `
+ Test that when one color texture subresource is bound to different bind groups, its list of
+ internal usages within one usage scope can only be a compatible usage list. For texture
+ subresources in bind groups, the compatible usage lists are {TEXTURE_BINDING} and
+ {STORAGE_BINDING}, which means it can only be bound as both TEXTURE_BINDING and STORAGE_BINDING in
+ different render pass encoders, otherwise a validation error will occur.`
+ )
+ .params(u =>
+ u
+ .combine('bg0Levels', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('bg0Layers', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('bg1Levels', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('bg1Layers', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('bgUsage0', ['texture', 'storage'] as const)
+ .combine('bgUsage1', ['texture', 'storage'] as const)
+ .unless(
+ t =>
+ (t.bgUsage0 === 'storage' && t.bg0Levels.count > 1) ||
+ (t.bgUsage1 === 'storage' && t.bg1Levels.count > 1)
+ )
+ .combine('inSamePass', [true, false])
+ )
+ .fn(async t => {
+ const { bg0Levels, bg0Layers, bg1Levels, bg1Layers, bgUsage0, bgUsage1, inSamePass } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ mipLevelCount: kTextureLevels,
+ });
+ const bg0 = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: bg0Layers.base,
+ arrayLayerCount: bg0Layers.count,
+ baseMipLevel: bg0Levels.base,
+ mipLevelCount: bg0Levels.count,
+ });
+ const bg1 = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: bg1Layers.base,
+ arrayLayerCount: bg1Layers.count,
+ baseMipLevel: bg1Levels.base,
+ mipLevelCount: bg1Levels.count,
+ });
+ const bindGroup0 = t.createBindGroupForTest(bg0, bgUsage0, 'float');
+ const bindGroup1 = t.createBindGroupForTest(bg1, bgUsage1, 'float');
+
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ mipLevelCount: 1,
+ });
+ const colorAttachment = t.getColorAttachment(colorTexture);
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment],
+ });
+ if (inSamePass) {
+ renderPass.setBindGroup(0, bindGroup0);
+ renderPass.setBindGroup(1, bindGroup1);
+ renderPass.end();
+ } else {
+ renderPass.setBindGroup(0, bindGroup0);
+ renderPass.end();
+
+ const renderPass2 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment],
+ });
+ renderPass2.setBindGroup(1, bindGroup1);
+ renderPass2.end();
+ }
+
+ const isMipLevelNotOverlapped = t.isRangeNotOverlapped(
+ bg0Levels.base,
+ bg0Levels.base + bg0Levels.count - 1,
+ bg1Levels.base,
+ bg1Levels.base + bg1Levels.count - 1
+ );
+ const isArrayLayerNotOverlapped = t.isRangeNotOverlapped(
+ bg0Layers.base,
+ bg0Layers.base + bg0Layers.count - 1,
+ bg1Layers.base,
+ bg1Layers.base + bg1Layers.count - 1
+ );
+ const isNotOverlapped = isMipLevelNotOverlapped || isArrayLayerNotOverlapped;
+
+ const success = !inSamePass || isNotOverlapped || bgUsage0 === bgUsage1;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources,depth_stencil_texture_in_bind_groups')
+ .desc(
+ `
+ Test that when one depth stencil texture subresource is bound to different bind groups, we can
+ always bind these two bind groups in either the same or different render pass encoder as the depth
+ stencil texture can only be bound as TEXTURE_BINDING in the bind group.`
+ )
+ .params(u =>
+ u
+ .combine('view0Levels', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('view0Layers', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('view1Levels', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('view1Layers', [
+ { base: 0, count: 1 },
+ { base: 1, count: 1 },
+ { base: 1, count: 2 },
+ ])
+ .combine('aspect0', ['depth-only', 'stencil-only'] as const)
+ .combine('aspect1', ['depth-only', 'stencil-only'] as const)
+ .combine('inSamePass', [true, false])
+ )
+ .fn(async t => {
+ const {
+ view0Levels,
+ view0Layers,
+ view1Levels,
+ view1Layers,
+ aspect0,
+ aspect1,
+ inSamePass,
+ } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'depth24plus-stencil8',
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ mipLevelCount: kTextureLevels,
+ });
+ const bindGroupView0 = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: view0Layers.base,
+ arrayLayerCount: view0Layers.count,
+ baseMipLevel: view0Levels.base,
+ mipLevelCount: view0Levels.count,
+ aspect: aspect0,
+ });
+ const bindGroupView1 = texture.createView({
+ dimension: '2d-array',
+ baseArrayLayer: view1Layers.base,
+ arrayLayerCount: view1Layers.count,
+ baseMipLevel: view1Levels.base,
+ mipLevelCount: view1Levels.count,
+ aspect: aspect1,
+ });
+
+ const sampleType0 = aspect0 === 'depth-only' ? 'depth' : 'uint';
+ const sampleType1 = aspect1 === 'depth-only' ? 'depth' : 'uint';
+ const bindGroup0 = t.createBindGroupForTest(bindGroupView0, 'texture', sampleType0);
+ const bindGroup1 = t.createBindGroupForTest(bindGroupView1, 'texture', sampleType1);
+
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ mipLevelCount: 1,
+ });
+ const colorAttachment = t.getColorAttachment(colorTexture);
+ const encoder = t.device.createCommandEncoder();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment],
+ });
+ if (inSamePass) {
+ renderPass.setBindGroup(0, bindGroup0);
+ renderPass.setBindGroup(1, bindGroup1);
+ renderPass.end();
+ } else {
+ renderPass.setBindGroup(0, bindGroup0);
+ renderPass.end();
+
+ const renderPass2 = encoder.beginRenderPass({
+ colorAttachments: [colorAttachment],
+ });
+ renderPass2.setBindGroup(1, bindGroup1);
+ renderPass2.end();
+ }
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts
new file mode 100644
index 0000000000..afde0863bb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts
@@ -0,0 +1,420 @@
+export const description = `
+Texture Usages Validation Tests on All Kinds of WebGPU Subresource Usage Scopes.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { unreachable } from '../../../../../common/util/util.js';
+import { ValidationTest } from '../../validation_test.js';
+
+class F extends ValidationTest {
+ createBindGroupLayoutForTest(
+ textureUsage: 'texture' | 'storage',
+ sampleType: 'float' | 'depth' | 'uint',
+ visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT']
+ ): GPUBindGroupLayout {
+ const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = {
+ binding: 0,
+ visibility,
+ };
+
+ switch (textureUsage) {
+ case 'texture':
+ bindGroupLayoutEntry.texture = { viewDimension: '2d-array', sampleType };
+ break;
+ case 'storage':
+ bindGroupLayoutEntry.storageTexture = {
+ access: 'write-only',
+ format: 'rgba8unorm',
+ viewDimension: '2d-array',
+ };
+ break;
+ default:
+ unreachable();
+ break;
+ }
+ return this.device.createBindGroupLayout({
+ entries: [bindGroupLayoutEntry],
+ });
+ }
+
+ createBindGroupForTest(
+ textureView: GPUTextureView,
+ textureUsage: 'texture' | 'storage',
+ sampleType: 'float' | 'depth' | 'uint',
+ visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT']
+ ) {
+ return this.device.createBindGroup({
+ layout: this.createBindGroupLayoutForTest(textureUsage, sampleType, visibility),
+ entries: [{ binding: 0, resource: textureView }],
+ });
+ }
+}
+
+export const g = makeTestGroup(F);
+
+const kTextureSize = 16;
+const kTextureLayers = 3;
+
+g.test('subresources,set_bind_group_on_same_index_color_texture')
+ .desc(
+ `
+ Test that when one color texture subresource is bound to different bind groups, whether the bind
+ groups are reset by another compatible ones or not, its list of internal usages within one usage
+ scope can only be a compatible usage list.`
+ )
+ .params(u =>
+ u
+ .combineWithParams([
+ { useDifferentTextureAsTexture2: true, baseLayer2: 0, view2Binding: 'texture' },
+ { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'texture' },
+ { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'texture' },
+ { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'storage' },
+ { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'storage' },
+ ] as const)
+ .combine('hasConflict', [true, false])
+ )
+ .fn(async t => {
+ const { useDifferentTextureAsTexture2, baseLayer2, view2Binding, hasConflict } = t.params;
+
+ const texture0 = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ });
+ // We always bind the first layer of the texture to bindGroup0.
+ const textureView0 = texture0.createView({
+ dimension: '2d-array',
+ baseArrayLayer: 0,
+ arrayLayerCount: 1,
+ });
+ const bindGroup0 = t.createBindGroupForTest(textureView0, view2Binding, 'float');
+
+ // In one renderPassEncoder it is an error to set both bindGroup0 and bindGroup1.
+ const view1Binding = hasConflict
+ ? view2Binding === 'texture'
+ ? 'storage'
+ : 'texture'
+ : view2Binding;
+ const bindGroup1 = t.createBindGroupForTest(textureView0, view1Binding, 'float');
+
+ const texture2 = useDifferentTextureAsTexture2
+ ? t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ })
+ : texture0;
+ const textureView2 = texture2.createView({
+ dimension: '2d-array',
+ baseArrayLayer: baseLayer2,
+ arrayLayerCount: kTextureLayers - baseLayer2,
+ });
+ // There should be no conflict between bindGroup0 and validBindGroup2.
+ const validBindGroup2 = t.createBindGroupForTest(textureView2, view2Binding, 'float');
+
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPassEncoder.setBindGroup(0, bindGroup0);
+ renderPassEncoder.setBindGroup(1, bindGroup1);
+ renderPassEncoder.setBindGroup(1, validBindGroup2);
+ renderPassEncoder.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, hasConflict);
+ });
+
+g.test('subresources,set_bind_group_on_same_index_depth_stencil_texture')
+ .desc(
+ `
+ Test that when one depth stencil texture subresource is bound to different bind groups, whether
+ the bind groups are reset by another compatible ones or not, its list of internal usages within
+ one usage scope can only be a compatible usage list.`
+ )
+ .params(u =>
+ u
+ .combine('bindAspect', ['depth-only', 'stencil-only'] as const)
+ .combine('depthStencilReadOnly', [true, false])
+ )
+ .fn(async t => {
+ const { bindAspect, depthStencilReadOnly } = t.params;
+ const depthStencilTexture = t.device.createTexture({
+ format: 'depth24plus-stencil8',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+
+ const conflictedToNonReadOnlyAttachmentBindGroup = t.createBindGroupForTest(
+ depthStencilTexture.createView({
+ dimension: '2d-array',
+ aspect: bindAspect,
+ }),
+ 'texture',
+ bindAspect === 'depth-only' ? 'depth' : 'uint'
+ );
+
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+ const validBindGroup = t.createBindGroupForTest(
+ colorTexture.createView({
+ dimension: '2d-array',
+ }),
+ 'texture',
+ 'float'
+ );
+
+ const encoder = t.device.createCommandEncoder();
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [],
+ depthStencilAttachment: {
+ view: depthStencilTexture.createView(),
+ depthReadOnly: depthStencilReadOnly,
+ stencilReadOnly: depthStencilReadOnly,
+ },
+ });
+ renderPassEncoder.setBindGroup(0, conflictedToNonReadOnlyAttachmentBindGroup);
+ renderPassEncoder.setBindGroup(0, validBindGroup);
+ renderPassEncoder.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !depthStencilReadOnly);
+ });
+
+g.test('subresources,set_unused_bind_group')
+ .desc(
+ `
+ Test that when one texture subresource is bound to different bind groups and the bind groups are
+ used in the same render or compute pass encoder, its list of internal usages within one usage
+ scope can only be a compatible usage list.`
+ )
+ .params(u => u.combine('inRenderPass', [true, false]).combine('hasConflict', [true, false]))
+ .fn(async t => {
+ const { inRenderPass, hasConflict } = t.params;
+
+ const texture0 = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING,
+ size: [kTextureSize, kTextureSize, kTextureLayers],
+ });
+ // We always bind the first layer of the texture to bindGroup0.
+ const textureView0 = texture0.createView({
+ dimension: '2d-array',
+ baseArrayLayer: 0,
+ arrayLayerCount: 1,
+ });
+ const visibility = inRenderPass ? GPUShaderStage.FRAGMENT : GPUShaderStage.COMPUTE;
+ // bindGroup0 is used by the pipelines, and bindGroup1 is not used by the pipelines.
+ const textureUsage0 = inRenderPass ? 'texture' : 'storage';
+ const textureUsage1 = hasConflict ? (inRenderPass ? 'storage' : 'texture') : textureUsage0;
+ const bindGroup0 = t.createBindGroupForTest(textureView0, textureUsage0, 'float', visibility);
+ const bindGroup1 = t.createBindGroupForTest(textureView0, textureUsage1, 'float', visibility);
+
+ const encoder = t.device.createCommandEncoder();
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [t.createBindGroupLayoutForTest(textureUsage0, 'float', visibility)],
+ });
+ if (inRenderPass) {
+ const renderPipeline = t.device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: t.device.createShaderModule({
+ code: t.getNoOpShaderCode('VERTEX'),
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var texture0 : texture_2d_array<f32>;
+ @fragment fn main()
+ -> @location(0) vec4<f32> {
+ return textureLoad(texture0, vec2<i32>(), 0, 0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ });
+
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ loadOp: 'load',
+ storeOp: 'store',
+ },
+ ],
+ });
+ renderPassEncoder.setBindGroup(0, bindGroup0);
+ renderPassEncoder.setBindGroup(1, bindGroup1);
+ renderPassEncoder.setPipeline(renderPipeline);
+ renderPassEncoder.draw(1);
+ renderPassEncoder.end();
+ } else {
+ const computePipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var texture0 : texture_storage_2d_array<rgba8unorm, write>;
+ @compute @workgroup_size(1)
+ fn main() {
+ textureStore(texture0, vec2<i32>(), 0, vec4<f32>());
+ }`,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ const computePassEncoder = encoder.beginComputePass();
+ computePassEncoder.setBindGroup(0, bindGroup0);
+ computePassEncoder.setBindGroup(1, bindGroup1);
+ computePassEncoder.setPipeline(computePipeline);
+ computePassEncoder.dispatchWorkgroups(1);
+ computePassEncoder.end();
+ }
+
+ // In WebGPU SPEC (Chapter 3.4.5, Synchronization):
+ // This specification defines the following usage scopes:
+ // - In a compute pass, each dispatch command (dispatchWorkgroups() or
+ // dispatchWorkgroupsIndirect()) is one usage scope. A subresource is "used" in the usage
+ // scope if it is potentially accessible by the command. State-setting compute pass commands,
+ // like setBindGroup(index, bindGroup, dynamicOffsets), do not contribute directly to a usage
+ // scope.
+ // - One render pass is one usage scope. A subresource is "used" in the usage scope if it’s
+ // referenced by any (state-setting or non-state-setting) command. For example, in
+ // setBindGroup(index, bindGroup, dynamicOffsets), every subresource in bindGroup is "used" in
+ // the render pass’s usage scope.
+ const success = !inRenderPass || !hasConflict;
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !success);
+ });
+
+g.test('subresources,texture_usages_in_copy_and_render_pass')
+ .desc(
+ `
+ Test that using one texture subresource in a render pass encoder and a copy command is always
+ allowed as WebGPU SPEC (chapter 3.4.5) defines that out of any pass encoder, each command always
+ belongs to one usage scope.`
+ )
+ .params(u =>
+ u
+ .combine('usage0', [
+ 'copy-src',
+ 'copy-dst',
+ 'texture',
+ 'storage',
+ 'color-attachment',
+ ] as const)
+ .combine('usage1', [
+ 'copy-src',
+ 'copy-dst',
+ 'texture',
+ 'storage',
+ 'color-attachment',
+ ] as const)
+ .filter(
+ ({ usage0, usage1 }) =>
+ usage0 === 'copy-src' ||
+ usage0 === 'copy-dst' ||
+ usage1 === 'copy-src' ||
+ usage1 === 'copy-dst'
+ )
+ )
+ .fn(async t => {
+ const { usage0, usage1 } = t.params;
+
+ const texture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.STORAGE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+
+ const UseTextureOnCommandEncoder = (
+ texture: GPUTexture,
+ usage: 'copy-src' | 'copy-dst' | 'texture' | 'storage' | 'color-attachment',
+ encoder: GPUCommandEncoder
+ ) => {
+ switch (usage) {
+ case 'copy-src': {
+ const buffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ encoder.copyTextureToBuffer({ texture }, { buffer }, [1, 1, 1]);
+ break;
+ }
+ case 'copy-dst': {
+ const buffer = t.createBufferWithState('valid', {
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ encoder.copyBufferToTexture({ buffer }, { texture }, [1, 1, 1]);
+ break;
+ }
+ case 'color-attachment': {
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [{ view: texture.createView(), loadOp: 'load', storeOp: 'store' }],
+ });
+ renderPassEncoder.end();
+ break;
+ }
+ case 'texture':
+ case 'storage': {
+ const colorTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ size: [kTextureSize, kTextureSize, 1],
+ });
+ const renderPassEncoder = encoder.beginRenderPass({
+ colorAttachments: [
+ { view: colorTexture.createView(), loadOp: 'load', storeOp: 'store' },
+ ],
+ });
+ const bindGroup = t.createBindGroupForTest(
+ texture.createView({
+ dimension: '2d-array',
+ }),
+ usage,
+ 'float'
+ );
+ renderPassEncoder.setBindGroup(0, bindGroup);
+ renderPassEncoder.end();
+ break;
+ }
+ }
+ };
+ const encoder = t.device.createCommandEncoder();
+ UseTextureOnCommandEncoder(texture, usage0, encoder);
+ UseTextureOnCommandEncoder(texture, usage1, encoder);
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, false);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts
new file mode 100644
index 0000000000..9729b69547
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/entry_point.spec.ts
@@ -0,0 +1,117 @@
+export const description = `
+This tests entry point validation of compute/render pipelines and their shader modules.
+
+The entryPoint in shader module include standard "main" and others.
+The entryPoint assigned in descriptor include:
+- Matching case (control case)
+- Empty string
+- Mistyping
+- Containing invalid char, including space and control codes (Null character)
+- Unicode entrypoints and their ASCIIfied version
+
+TODO:
+- Test unicode normalization (gpuweb/gpuweb#1160)
+- Fine-tune test cases to reduce number by removing trivially similiar cases
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kDefaultVertexShaderCode, getShaderWithEntryPoint } from '../../../util/shader.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+const kEntryPointTestCases = [
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'main' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: '' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'main\0' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'main\0a' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'mian' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'main ' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'ma in' },
+ { shaderModuleEntryPoint: 'main', stageEntryPoint: 'main\n' },
+ { shaderModuleEntryPoint: 'mian', stageEntryPoint: 'mian' },
+ { shaderModuleEntryPoint: 'mian', stageEntryPoint: 'main' },
+ { shaderModuleEntryPoint: 'mainmain', stageEntryPoint: 'mainmain' },
+ { shaderModuleEntryPoint: 'mainmain', stageEntryPoint: 'foo' },
+ { shaderModuleEntryPoint: 'main_t12V3', stageEntryPoint: 'main_t12V3' },
+ { shaderModuleEntryPoint: 'main_t12V3', stageEntryPoint: 'main_t12V5' },
+ { shaderModuleEntryPoint: 'main_t12V3', stageEntryPoint: '_main_t12V3' },
+ { shaderModuleEntryPoint: 'séquençage', stageEntryPoint: 'séquençage' },
+ { shaderModuleEntryPoint: 'séquençage', stageEntryPoint: 'sequencage' },
+];
+
+g.test('compute')
+ .desc(
+ `
+Tests calling createComputePipeline(Async) with valid vertex stage shader and different entryPoints,
+and check that the APIs only accept matching entryPoint.
+`
+ )
+ .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases))
+ .fn(async t => {
+ const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params;
+ const descriptor: GPUComputePipelineDescriptor = {
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: getShaderWithEntryPoint('compute', shaderModuleEntryPoint),
+ }),
+ entryPoint: stageEntryPoint,
+ },
+ };
+ const _success = shaderModuleEntryPoint === stageEntryPoint;
+ t.doCreateComputePipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('vertex')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) with valid vertex stage shader and different entryPoints,
+and check that the APIs only accept matching entryPoint.
+`
+ )
+ .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases))
+ .fn(async t => {
+ const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params;
+ const descriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: getShaderWithEntryPoint('vertex', shaderModuleEntryPoint),
+ }),
+ entryPoint: stageEntryPoint,
+ },
+ };
+ const _success = shaderModuleEntryPoint === stageEntryPoint;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
+
+g.test('fragment')
+ .desc(
+ `
+Tests calling createRenderPipeline(Async) with valid fragment stage shader and different entryPoints,
+and check that the APIs only accept matching entryPoint.
+`
+ )
+ .params(u => u.combine('isAsync', [true, false]).combineWithParams(kEntryPointTestCases))
+ .fn(async t => {
+ const { isAsync, shaderModuleEntryPoint, stageEntryPoint } = t.params;
+ const descriptor: GPURenderPipelineDescriptor = {
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: kDefaultVertexShaderCode,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: getShaderWithEntryPoint('fragment', shaderModuleEntryPoint),
+ }),
+ entryPoint: stageEntryPoint,
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ };
+ const _success = shaderModuleEntryPoint === stageEntryPoint;
+ t.doCreateRenderPipelineTest(isAsync, _success, descriptor);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/overrides.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/overrides.spec.ts
new file mode 100644
index 0000000000..ac3645a4a4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/shader_module/overrides.spec.ts
@@ -0,0 +1,96 @@
+export const description = `
+This tests overrides numeric identifiers should not conflict.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('id_conflict')
+ .desc(
+ `
+Tests that overrides' explicit numeric identifier should not conflict.
+`
+ )
+ .fn(async t => {
+ t.expectValidationError(() => {
+ t.device.createShaderModule({
+ code: `
+@id(1234) override c0: u32;
+@id(4321) override c1: u32;
+
+@compute @workgroup_size(1) fn main() {
+ // make sure the overridable constants are not optimized out
+ _ = c0;
+ _ = c1;
+}
+ `,
+ });
+ }, false);
+
+ t.expectValidationError(() => {
+ t.device.createShaderModule({
+ code: `
+@id(1234) override c0: u32;
+@id(1234) override c1: u32;
+
+@compute @workgroup_size(1) fn main() {
+ // make sure the overridable constants are not optimized out
+ _ = c0;
+ _ = c1;
+}
+ `,
+ });
+ }, true);
+ });
+
+g.test('name_conflict')
+ .desc(
+ `
+Tests that overrides' variable name should not conflict, regardless of their numeric identifiers.
+`
+ )
+ .fn(async t => {
+ t.expectValidationError(() => {
+ t.device.createShaderModule({
+ code: `
+override c0: u32;
+override c0: u32;
+
+@compute @workgroup_size(1) fn main() {
+ // make sure the overridable constants are not optimized out
+ _ = c0;
+}
+ `,
+ });
+ }, true);
+
+ t.expectValidationError(() => {
+ t.device.createShaderModule({
+ code: `
+@id(1) override c0: u32;
+override c0: u32;
+
+@compute @workgroup_size(1) fn main() {
+ // make sure the overridable constants are not optimized out
+ _ = c0;
+}
+ `,
+ });
+ }, true);
+
+ t.expectValidationError(() => {
+ t.device.createShaderModule({
+ code: `
+@id(1) override c0: u32;
+@id(2) override c0: u32;
+
+@compute @workgroup_size(1) fn main() {
+ // make sure the overridable constants are not optimized out
+ _ = c0;
+}
+ `,
+ });
+ }, true);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/README.txt
new file mode 100644
index 0000000000..319cc76e5c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/README.txt
@@ -0,0 +1,5 @@
+Tests of behavior while the device is lost.
+
+- x= every method in the API.
+
+TODO: implement
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts
new file mode 100644
index 0000000000..a50469db2a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/state/device_lost/destroy.spec.ts
@@ -0,0 +1,962 @@
+export const description = `
+Tests for device lost induced via destroy.
+ - Tests that prior to device destruction, valid APIs do not generate errors (control case).
+ - After device destruction, runs the same APIs. No expected observable results, so test crash or future failures are the only current failure indicators.
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import {
+ allBindingEntries,
+ bindingTypeInfo,
+ kBindableResources,
+ kBufferUsageKeys,
+ kBufferUsageInfo,
+ kBufferUsageCopy,
+ kBufferUsageCopyInfo,
+ kCompressedTextureFormats,
+ kQueryTypes,
+ kTextureUsageType,
+ kTextureUsageTypeInfo,
+ kTextureUsageCopy,
+ kTextureUsageCopyInfo,
+ kRegularTextureFormats,
+ kRenderableColorTextureFormats,
+ kShaderStageKeys,
+ kTextureFormatInfo,
+} from '../../../../capability_info.js';
+import { CommandBufferMaker, EncoderType } from '../../../../util/command_buffer_maker.js';
+import {
+ canCopyFromCanvasContext,
+ createCanvas,
+ kAllCanvasTypes,
+ kValidCanvasContextIds,
+} from '../../../../util/create_elements.js';
+import { ValidationTest } from '../../validation_test.js';
+
+const kCommandValidationStages = ['finish', 'submit'];
+type CommandValidationStage = typeof kCommandValidationStages[number];
+
+class DeviceDestroyTests extends ValidationTest {
+ /**
+ * Expects that `fn` does not produce any errors before the device is destroyed, and then calls
+ * `fn` after the device is destroyed without any specific expectation. If `awaitLost` is true, we
+ * also wait for device.lost to resolve before executing `fn` in the destroy case.
+ */
+ async executeAfterDestroy(fn: () => void, awaitLost: boolean): Promise<void> {
+ this.expectDeviceLost('destroyed');
+
+ this.expectValidationError(fn, false);
+ this.device.destroy();
+ if (awaitLost) {
+ const lostInfo = await this.device.lost;
+ this.expect(lostInfo.reason === 'destroyed');
+ }
+ fn();
+ }
+
+ /**
+ * Expects that encoders can finish and submit the resulting commands before the device is
+ * destroyed, then repeats the same process after the device is destroyed without any specific
+ * expectations.
+ * There are two valid stages: 'finish' and 'submit'.
+ * 'finish': Tests [encode, finish] and [encoder, destroy, finish]
+ * 'submit': Tests [encoder, finish, submit] and [encoder, finish, destroy, submit]
+ */
+ async executeCommandsAfterDestroy<T extends EncoderType>(
+ stage: CommandValidationStage,
+ awaitLost: boolean,
+ encoderType: T,
+ fn: (maker: CommandBufferMaker<T>) => CommandBufferMaker<T>
+ ): Promise<void> {
+ this.expectDeviceLost('destroyed');
+
+ switch (stage) {
+ case 'finish': {
+ // Control case
+ fn(this.createEncoder(encoderType)).validateFinish(true);
+ // Validation case
+ const encoder = fn(this.createEncoder(encoderType));
+ await this.executeAfterDestroy(() => {
+ encoder.finish();
+ }, awaitLost);
+ break;
+ }
+ case 'submit': {
+ // Control case
+ fn(this.createEncoder(encoderType)).validateFinishAndSubmit(true, true);
+ // Validation case
+ const commands = fn(this.createEncoder(encoderType)).validateFinish(true);
+ await this.executeAfterDestroy(() => {
+ this.queue.submit([commands]);
+ }, awaitLost);
+ break;
+ }
+ }
+ }
+}
+
+export const g = makeTestGroup(DeviceDestroyTests);
+
+g.test('createBuffer')
+ .desc(
+ `
+Tests creating buffers on destroyed device. Tests valid combinations of:
+ - Various usages
+ - Mapped at creation or not
+ `
+ )
+ .params(u =>
+ u
+ .combine('usageType', kBufferUsageKeys)
+ .beginSubcases()
+ .combine('usageCopy', kBufferUsageCopy)
+ .combine('awaitLost', [true, false])
+ .filter(({ usageType, usageCopy }) => {
+ if (usageType === 'COPY_SRC' || usageType === 'COPY_DST') {
+ return false;
+ }
+ if (usageType === 'MAP_READ') {
+ return usageCopy === 'COPY_NONE' || usageCopy === 'COPY_DST';
+ }
+ if (usageType === 'MAP_WRITE') {
+ return usageCopy === 'COPY_NONE' || usageCopy === 'COPY_SRC';
+ }
+ return true;
+ })
+ .combine('mappedAtCreation', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, usageType, usageCopy, mappedAtCreation } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createBuffer({
+ size: 16,
+ usage: kBufferUsageInfo[usageType] | kBufferUsageCopyInfo[usageCopy],
+ mappedAtCreation,
+ });
+ }, awaitLost);
+ });
+
+g.test('createTexture,2d,uncompressed_format')
+ .desc(
+ `
+Tests creating 2d uncompressed textures on destroyed device. Tests valid combinations of:
+ - Various uncompressed texture formats
+ - Various usages
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRegularTextureFormats)
+ .beginSubcases()
+ .combine('usageType', kTextureUsageType)
+ .combine('usageCopy', kTextureUsageCopy)
+ .combine('awaitLost', [true, false])
+ .filter(({ format, usageType }) => {
+ const info = kTextureFormatInfo[format];
+ return !(
+ (!info.renderable && usageType === 'render') ||
+ (!info.storage && usageType === 'storage')
+ );
+ })
+ )
+ .fn(async t => {
+ const { awaitLost, format, usageType, usageCopy } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+ await t.executeAfterDestroy(() => {
+ t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: kTextureUsageTypeInfo[usageType] | kTextureUsageCopyInfo[usageCopy],
+ format,
+ });
+ }, awaitLost);
+ });
+
+g.test('createTexture,2d,compressed_format')
+ .desc(
+ `
+Tests creating 2d compressed textures on destroyed device. Tests valid combinations of:
+ - Various compressed texture formats
+ - Various usages
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kCompressedTextureFormats)
+ .beginSubcases()
+ .combine('usageType', kTextureUsageType)
+ .combine('usageCopy', kTextureUsageCopy)
+ .combine('awaitLost', [true, false])
+ .filter(({ format, usageType }) => {
+ const info = kTextureFormatInfo[format];
+ return !(
+ (!info.renderable && usageType === 'render') ||
+ (!info.storage && usageType === 'storage')
+ );
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { awaitLost, format, usageType, usageCopy } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+ await t.executeAfterDestroy(() => {
+ t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: kTextureUsageTypeInfo[usageType] | kTextureUsageCopyInfo[usageCopy],
+ format,
+ });
+ }, awaitLost);
+ });
+
+g.test('createView,2d,uncompressed_format')
+ .desc(
+ `
+Tests creating texture views on 2d uncompressed textures from destroyed device. Tests valid combinations of:
+ - Various uncompressed texture formats
+ - Various usages
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRegularTextureFormats)
+ .beginSubcases()
+ .combine('usageType', kTextureUsageType)
+ .combine('usageCopy', kTextureUsageCopy)
+ .combine('awaitLost', [true, false])
+ .filter(({ format, usageType }) => {
+ const info = kTextureFormatInfo[format];
+ return !(
+ (!info.renderable && usageType === 'render') ||
+ (!info.storage && usageType === 'storage')
+ );
+ })
+ )
+ .fn(async t => {
+ const { awaitLost, format, usageType, usageCopy } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+ const texture = t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: kTextureUsageTypeInfo[usageType] | kTextureUsageCopyInfo[usageCopy],
+ format,
+ });
+ await t.executeAfterDestroy(() => {
+ texture.createView({ format });
+ }, awaitLost);
+ });
+
+g.test('createView,2d,compressed_format')
+ .desc(
+ `
+Tests creating texture views on 2d compressed textures from destroyed device. Tests valid combinations of:
+ - Various compressed texture formats
+ - Various usages
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kCompressedTextureFormats)
+ .beginSubcases()
+ .combine('usageType', kTextureUsageType)
+ .combine('usageCopy', kTextureUsageCopy)
+ .combine('awaitLost', [true, false])
+ .filter(({ format, usageType }) => {
+ const info = kTextureFormatInfo[format];
+ return !(
+ (!info.renderable && usageType === 'render') ||
+ (!info.storage && usageType === 'storage')
+ );
+ })
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { awaitLost, format, usageType, usageCopy } = t.params;
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+ const texture = t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: kTextureUsageTypeInfo[usageType] | kTextureUsageCopyInfo[usageCopy],
+ format,
+ });
+ await t.executeAfterDestroy(() => {
+ texture.createView({ format });
+ }, awaitLost);
+ });
+
+g.test('createSampler')
+ .desc(
+ `
+Tests creating samplers on destroyed device.
+ `
+ )
+ .params(u => u.beginSubcases().combine('awaitLost', [true, false]))
+ .fn(async t => {
+ const { awaitLost } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createSampler();
+ }, awaitLost);
+ });
+
+g.test('createBindGroupLayout')
+ .desc(
+ `
+Tests creating bind group layouts on destroyed device. Tests valid combinations of:
+ - Various valid binding entries
+ - Maximum set of visibility for each binding entry
+ `
+ )
+ .params(u =>
+ u.combine('entry', allBindingEntries(false)).beginSubcases().combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, entry } = t.params;
+ const visibility = bindingTypeInfo(entry).validStages;
+ await t.executeAfterDestroy(() => {
+ t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility, ...entry }],
+ });
+ }, awaitLost);
+ });
+
+g.test('createBindGroup')
+ .desc(
+ `
+Tests creating bind group on destroyed device. Tests valid combinations of:
+ - Various binded resource types
+ - Various valid binding entries
+ - Maximum set of visibility for each binding entry
+ `
+ )
+ .desc(`A destroyed device should not be able to create any valid bind groups.`)
+ .params(u =>
+ u
+ .combine('resourceType', kBindableResources)
+ .combine('entry', allBindingEntries(false))
+ .filter(({ resourceType, entry }) => {
+ const info = bindingTypeInfo(entry);
+ switch (info.resource) {
+ // Either type of sampler may be bound to a filtering sampler binding.
+ case 'filtSamp':
+ return resourceType === 'filtSamp' || resourceType === 'nonFiltSamp';
+ // But only non-filtering samplers can be used with non-filtering sampler bindings.
+ case 'nonFiltSamp':
+ return resourceType === 'nonFiltSamp';
+ default:
+ return info.resource === resourceType;
+ }
+ })
+ .beginSubcases()
+ .combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, resourceType, entry } = t.params;
+ const visibility = bindingTypeInfo(entry).validStages;
+ const layout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility, ...entry }],
+ });
+ const resource = t.getBindingResource(resourceType);
+ await t.executeAfterDestroy(() => {
+ t.device.createBindGroup({ layout, entries: [{ binding: 0, resource }] });
+ }, awaitLost);
+ });
+
+g.test('createPipelineLayout')
+ .desc(
+ `
+Tests creating pipeline layouts on destroyed device. Tests valid combinations of:
+ - Various bind groups with valid binding entries
+ - Maximum set of visibility for each binding entry
+ `
+ )
+ .params(u =>
+ u.combine('entry', allBindingEntries(false)).beginSubcases().combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, entry } = t.params;
+ const visibility = bindingTypeInfo(entry).validStages;
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility, ...entry }],
+ });
+ await t.executeAfterDestroy(() => {
+ t.device.createPipelineLayout({
+ bindGroupLayouts: [bindGroupLayout],
+ });
+ }, awaitLost);
+ });
+
+g.test('createShaderModule')
+ .desc(
+ `
+Tests creating shader modules on destroyed device.
+ - Tests all shader stages: vertex, fragment, compute
+ `
+ )
+ .params(u =>
+ u.combine('stage', kShaderStageKeys).beginSubcases().combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, stage } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createShaderModule({ code: t.getNoOpShaderCode(stage) });
+ }, awaitLost);
+ });
+
+g.test('createComputePipeline')
+ .desc(
+ `
+Tests creating compute pipeline on destroyed device.
+ - Tests with a valid no-op compute shader
+ `
+ )
+ .params(u => u.beginSubcases().combine('awaitLost', [true, false]))
+ .fn(async t => {
+ const { awaitLost } = t.params;
+ const cShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('COMPUTE') });
+ await t.executeAfterDestroy(() => {
+ t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module: cShader, entryPoint: 'main' },
+ });
+ }, awaitLost);
+ });
+
+g.test('createRenderPipeline')
+ .desc(
+ `
+Tests creating render pipeline on destroyed device.
+ - Tests with valid no-op vertex and fragment shaders
+ `
+ )
+ .params(u => u.beginSubcases().combine('awaitLost', [true, false]))
+ .fn(async t => {
+ const { awaitLost } = t.params;
+ const vShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('VERTEX') });
+ const fShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('FRAGMENT') });
+ await t.executeAfterDestroy(() => {
+ t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module: vShader, entryPoint: 'main' },
+ fragment: {
+ module: fShader,
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ });
+ }, awaitLost);
+ });
+
+g.test('createCommandEncoder')
+ .desc(
+ `
+Tests creating command encoders on destroyed device.
+ `
+ )
+ .params(u => u.beginSubcases().combine('awaitLost', [true, false]))
+ .fn(async t => {
+ const { awaitLost } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createCommandEncoder();
+ }, awaitLost);
+ });
+
+g.test('createRenderBundleEncoder')
+ .desc(
+ `
+Tests creating render bundle encoders on destroyed device.
+ - Tests various renderable texture color formats
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kRenderableColorTextureFormats)
+ .beginSubcases()
+ .combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { awaitLost, format } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createRenderBundleEncoder({ colorFormats: [format] });
+ }, awaitLost);
+ });
+
+g.test('createQuerySet')
+ .desc(
+ `
+Tests creating query sets on destroyed device.
+ - Tests various query set types
+ `
+ )
+ .params(u => u.combine('type', kQueryTypes).beginSubcases().combine('awaitLost', [true, false]))
+ .beforeAllSubcases(t => {
+ const { type } = t.params;
+ t.selectDeviceForQueryTypeOrSkipTestCase(type);
+ })
+ .fn(async t => {
+ const { awaitLost, type } = t.params;
+ await t.executeAfterDestroy(() => {
+ t.device.createQuerySet({ type, count: 4 });
+ }, awaitLost);
+ });
+
+g.test('command,copyBufferToBuffer')
+ .desc(
+ `
+Tests copyBufferToBuffer command with various uncompressed formats on destroyed device.
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const kBufferSize = 16;
+ const src = t.device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const dst = t.device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.copyBufferToBuffer(src, 0, dst, 0, kBufferSize);
+ return maker;
+ });
+ });
+
+g.test('command,copyBufferToTexture')
+ .desc(
+ `
+Tests copyBufferToTexture command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const format = 'rgba32uint';
+ const { bytesPerBlock, blockWidth, blockHeight } = kTextureFormatInfo[format];
+ const src = {
+ buffer: t.device.createBuffer({
+ size: bytesPerBlock,
+ usage: GPUBufferUsage.COPY_SRC,
+ }),
+ };
+ const dst = {
+ texture: t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUTextureUsage.COPY_DST,
+ format,
+ }),
+ };
+ const copySize = { width: blockWidth, height: blockHeight };
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.copyBufferToTexture(src, dst, copySize);
+ return maker;
+ });
+ });
+
+g.test('command,copyTextureToBuffer')
+ .desc(
+ `
+Tests copyTextureToBuffer command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const format = 'rgba32uint';
+ const { bytesPerBlock, blockWidth, blockHeight } = kTextureFormatInfo[format];
+ const src = {
+ texture: t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUTextureUsage.COPY_SRC,
+ format,
+ }),
+ };
+ const dst = {
+ buffer: t.device.createBuffer({
+ size: bytesPerBlock,
+ usage: GPUBufferUsage.COPY_DST,
+ }),
+ };
+ const copySize = { width: blockWidth, height: blockHeight };
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.copyTextureToBuffer(src, dst, copySize);
+ return maker;
+ });
+ });
+
+g.test('command,copyTextureToTexture')
+ .desc(
+ `
+Tests copyTextureToTexture command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const format = 'rgba32uint';
+ const { blockWidth, blockHeight } = kTextureFormatInfo[format];
+ const src = {
+ texture: t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUTextureUsage.COPY_SRC,
+ format,
+ }),
+ };
+ const dst = {
+ texture: t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUBufferUsage.COPY_DST,
+ format,
+ }),
+ };
+ const copySize = { width: blockWidth, height: blockHeight };
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.copyTextureToTexture(src, dst, copySize);
+ return maker;
+ });
+ });
+
+g.test('command,clearBuffer')
+ .desc(
+ `
+Tests encoding and finishing a clearBuffer command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const kBufferSize = 16;
+ const buffer = t.device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.clearBuffer(buffer, 0, kBufferSize);
+ return maker;
+ });
+ });
+
+g.test('command,writeTimestamp')
+ .desc(
+ `
+Tests encoding and finishing a writeTimestamp command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u
+ .combine('type', kQueryTypes)
+ .beginSubcases()
+ .combine('stage', kCommandValidationStages)
+ .combine('awaitLost', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { type } = t.params;
+
+ // writeTimestamp is only available for devices that enable the 'timestamp-query' feature.
+ const queryTypes: GPUQueryType[] = ['timestamp'];
+ if (type !== 'timestamp') {
+ queryTypes.push(type);
+ }
+
+ t.selectDeviceForQueryTypeOrSkipTestCase(queryTypes);
+ })
+ .fn(async t => {
+ const { type, stage, awaitLost } = t.params;
+ const querySet = t.device.createQuerySet({ type, count: 2 });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.writeTimestamp(querySet, 0);
+ return maker;
+ });
+ });
+
+g.test('command,resolveQuerySet')
+ .desc(
+ `
+Tests encoding and finishing a resolveQuerySet command on destroyed device.
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const kQueryCount = 2;
+ const querySet = t.createQuerySetWithState('valid');
+ const destination = t.createBufferWithState('valid', {
+ size: kQueryCount * 8,
+ usage: GPUBufferUsage.QUERY_RESOLVE,
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'non-pass', maker => {
+ maker.encoder.resolveQuerySet(querySet, 0, 1, destination, 0);
+ return maker;
+ });
+ });
+
+g.test('command,computePass,dispatch')
+ .desc(
+ `
+Tests encoding and dispatching a simple valid compute pass on destroyed device.
+ - Binds valid pipeline and bindgroups, then dispatches
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const cShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('COMPUTE') });
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module: cShader, entryPoint: 'main' },
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'compute pass', maker => {
+ maker.encoder.setPipeline(pipeline);
+ maker.encoder.dispatchWorkgroups(1);
+ return maker;
+ });
+ });
+
+g.test('command,renderPass,draw')
+ .desc(
+ `
+Tests encoding and finishing a simple valid render pass on destroyed device.
+ - Binds valid pipeline and bindgroups, then draws
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const vShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('VERTEX') });
+ const fShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('FRAGMENT') });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module: vShader, entryPoint: 'main' },
+ fragment: {
+ module: fShader,
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'render pass', maker => {
+ maker.encoder.setPipeline(pipeline);
+ maker.encoder.draw(0);
+ return maker;
+ });
+ });
+
+g.test('command,renderPass,renderBundle')
+ .desc(
+ `
+Tests encoding and drawing a render pass including a render bundle on destroyed device.
+ - Binds valid pipeline and bindgroups, executes render bundle, then draws
+ - Tests finishing encoding on destroyed device
+ - Tests submitting command on destroyed device
+ `
+ )
+ .params(u =>
+ u.beginSubcases().combine('stage', kCommandValidationStages).combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { stage, awaitLost } = t.params;
+ const vShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('VERTEX') });
+ const fShader = t.device.createShaderModule({ code: t.getNoOpShaderCode('FRAGMENT') });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: { module: vShader, entryPoint: 'main' },
+ fragment: {
+ module: fShader,
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ });
+ await t.executeCommandsAfterDestroy(stage, awaitLost, 'render bundle', maker => {
+ maker.encoder.setPipeline(pipeline);
+ maker.encoder.draw(0);
+ return maker;
+ });
+ });
+
+g.test('queue,writeBuffer')
+ .desc(
+ `
+Tests writeBuffer on queue on destroyed device.
+ `
+ )
+ .params(u =>
+ u.combine('numElements', [4, 8, 16]).beginSubcases().combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { numElements, awaitLost } = t.params;
+ const buffer = t.device.createBuffer({
+ size: numElements,
+ usage: GPUBufferUsage.COPY_DST,
+ });
+ const data = new Uint8Array(numElements);
+ await t.executeAfterDestroy(() => {
+ t.device.queue.writeBuffer(buffer, 0, data);
+ }, awaitLost);
+ });
+
+g.test('queue,writeTexture,2d,uncompressed_format')
+ .desc(
+ `
+Tests writeTexture on queue on destroyed device with uncompressed formats.
+ `
+ )
+ .params(u =>
+ u.combine('format', kRegularTextureFormats).beginSubcases().combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { format, awaitLost } = t.params;
+ const { blockWidth, blockHeight, bytesPerBlock } = kTextureFormatInfo[format];
+ const data = new Uint8Array(bytesPerBlock);
+ const texture = t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUTextureUsage.COPY_DST,
+ format,
+ });
+ await t.executeAfterDestroy(() => {
+ t.device.queue.writeTexture(
+ { texture },
+ data,
+ {},
+ { width: blockWidth, height: blockHeight }
+ );
+ }, awaitLost);
+ });
+
+g.test('queue,writeTexture,2d,compressed_format')
+ .desc(
+ `
+Tests writeTexture on queue on destroyed device with compressed formats.
+ `
+ )
+ .params(u =>
+ u
+ .combine('format', kCompressedTextureFormats)
+ .beginSubcases()
+ .combine('awaitLost', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { format } = t.params;
+ t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature);
+ })
+ .fn(async t => {
+ const { format, awaitLost } = t.params;
+ const { blockWidth, blockHeight, bytesPerBlock } = kTextureFormatInfo[format];
+ const data = new Uint8Array(bytesPerBlock);
+ const texture = t.device.createTexture({
+ size: { width: blockWidth, height: blockHeight },
+ usage: GPUTextureUsage.COPY_DST,
+ format,
+ });
+ await t.executeAfterDestroy(() => {
+ t.device.queue.writeTexture(
+ { texture },
+ data,
+ {},
+ { width: blockWidth, height: blockHeight }
+ );
+ }, awaitLost);
+ });
+
+g.test('queue,copyExternalImageToTexture,canvas')
+ .desc(
+ `
+Tests copyExternalImageToTexture from canvas on queue on destroyed device.
+ `
+ )
+ .params(u =>
+ u
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('contextType', kValidCanvasContextIds)
+ .filter(({ contextType }) => {
+ return canCopyFromCanvasContext(contextType);
+ })
+ .beginSubcases()
+ .combine('awaitLost', [true, false])
+ )
+ .fn(async t => {
+ const { canvasType, contextType, awaitLost } = t.params;
+ const canvas = createCanvas(t, canvasType, 1, 1);
+ const texture = t.device.createTexture({
+ size: { width: 1, height: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ const ctx = ((canvas as unknown) as HTMLCanvasElement).getContext(contextType);
+ if (ctx === null) {
+ t.skip('Failed to get context for canvas element');
+ return;
+ }
+ t.tryTrackForCleanup(ctx);
+
+ await t.executeAfterDestroy(() => {
+ t.device.queue.copyExternalImageToTexture(
+ { source: canvas },
+ { texture },
+ { width: 1, height: 1 }
+ );
+ }, awaitLost);
+ });
+
+g.test('queue,copyExternalImageToTexture,imageBitmap')
+ .desc(
+ `
+Tests copyExternalImageToTexture from canvas on queue on destroyed device.
+ `
+ )
+ .params(u => u.beginSubcases().combine('awaitLost', [true, false]))
+ .fn(async t => {
+ const { awaitLost } = t.params;
+ if (typeof createImageBitmap === 'undefined') {
+ t.skip('Creating ImageBitmaps is not supported.');
+ }
+ const imageBitmap = await createImageBitmap(new ImageData(new Uint8ClampedArray(4), 1, 1));
+
+ const texture = t.device.createTexture({
+ size: { width: 1, height: 1 },
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST,
+ });
+
+ await t.executeAfterDestroy(() => {
+ t.device.queue.copyExternalImageToTexture(
+ { source: imageBitmap },
+ { texture },
+ { width: 1, height: 1 }
+ );
+ }, awaitLost);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/destroy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/destroy.spec.ts
new file mode 100644
index 0000000000..2d3103decd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/destroy.spec.ts
@@ -0,0 +1,119 @@
+export const description = `
+Destroying a texture more than once is allowed.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { kTextureAspects, kTextureFormatInfo } from '../../../capability_info.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('base')
+ .desc(`Test that it is valid to destroy a texture.`)
+ .fn(t => {
+ const texture = t.getSampledTexture();
+ texture.destroy();
+ });
+
+g.test('twice')
+ .desc(`Test that it is valid to destroy a destroyed texture.`)
+ .fn(t => {
+ const texture = t.getSampledTexture();
+ texture.destroy();
+ texture.destroy();
+ });
+
+g.test('submit_a_destroyed_texture_as_attachment')
+ .desc(
+ `
+Test that it is invalid to submit with a texture as {color, depth, stencil, depth-stencil} attachment
+that was destroyed {before, after} encoding finishes.
+`
+ )
+ .params(u =>
+ u //
+ .combine('depthStencilTextureAspect', kTextureAspects)
+ .combine('colorTextureState', [
+ 'valid',
+ 'destroyedBeforeEncode',
+ 'destroyedAfterEncode',
+ ] as const)
+ .combine('depthStencilTextureState', [
+ 'valid',
+ 'destroyedBeforeEncode',
+ 'destroyedAfterEncode',
+ ] as const)
+ )
+ .fn(async t => {
+ const { colorTextureState, depthStencilTextureAspect, depthStencilTextureState } = t.params;
+
+ const isSubmitSuccess = colorTextureState === 'valid' && depthStencilTextureState === 'valid';
+
+ const colorTextureFormat: GPUTextureFormat = 'rgba32float';
+ const depthStencilTextureFormat: GPUTextureFormat =
+ depthStencilTextureAspect === 'all'
+ ? 'depth24plus-stencil8'
+ : depthStencilTextureAspect === 'depth-only'
+ ? 'depth32float'
+ : 'stencil8';
+
+ const colorTextureDesc: GPUTextureDescriptor = {
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: colorTextureFormat,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ };
+
+ const depthStencilTextureDesc: GPUTextureDescriptor = {
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: depthStencilTextureFormat,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ };
+
+ const colorTexture = t.device.createTexture(colorTextureDesc);
+ const depthStencilTexture = t.device.createTexture(depthStencilTextureDesc);
+
+ if (colorTextureState === 'destroyedBeforeEncode') {
+ colorTexture.destroy();
+ }
+ if (depthStencilTextureState === 'destroyedBeforeEncode') {
+ depthStencilTexture.destroy();
+ }
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
+ view: depthStencilTexture.createView({ aspect: depthStencilTextureAspect }),
+ };
+ if (kTextureFormatInfo[depthStencilTextureFormat].depth) {
+ depthStencilAttachment.depthClearValue = 0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'discard';
+ }
+ if (kTextureFormatInfo[depthStencilTextureFormat].stencil) {
+ depthStencilAttachment.stencilClearValue = 0;
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'discard';
+ }
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment,
+ });
+ renderPass.end();
+
+ const cmd = commandEncoder.finish();
+
+ if (colorTextureState === 'destroyedAfterEncode') {
+ colorTexture.destroy();
+ }
+ if (depthStencilTextureState === 'destroyedAfterEncode') {
+ depthStencilTexture.destroy();
+ }
+
+ t.expectValidationError(() => t.queue.submit([cmd]), !isSubmitSuccess);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/rg11b10ufloat_renderable.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/rg11b10ufloat_renderable.spec.ts
new file mode 100644
index 0000000000..3b9b58ffa7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/texture/rg11b10ufloat_renderable.spec.ts
@@ -0,0 +1,108 @@
+export const description = `
+Tests for capabilities added by rg11b10ufloat-renderable flag.
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUConst } from '../../../constants.js';
+import { ValidationTest } from '../validation_test.js';
+
+export const g = makeTestGroup(ValidationTest);
+
+g.test('create_texture')
+ .desc(
+ `
+Test that it is valid to create rg11b10ufloat texture with RENDER_ATTACHMENT usage and/or
+sampleCount > 1, iff rg11b10ufloat-renderable feature is enabled.
+Note, the createTexture tests cover these validation cases where this feature is not enabled.
+`
+ )
+ .params(u => u.combine('sampleCount', [1, 4]))
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase('rg11b10ufloat-renderable');
+ })
+ .fn(async t => {
+ const { sampleCount } = t.params;
+ const descriptor = {
+ size: [1, 1, 1],
+ format: 'rg11b10ufloat' as const,
+ sampleCount,
+ usage: GPUConst.TextureUsage.RENDER_ATTACHMENT,
+ };
+ t.device.createTexture(descriptor);
+ });
+
+g.test('begin_render_pass')
+ .desc(
+ `
+Test that it is valid to begin render pass with rg11b10ufloat texture format
+iff rg11b10ufloat-renderable feature is enabled.
+`
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase('rg11b10ufloat-renderable');
+ })
+ .fn(async t => {
+ const texture = t.device.createTexture({
+ size: [1, 1, 1],
+ format: 'rg11b10ufloat',
+ sampleCount: 1,
+ usage: GPUConst.TextureUsage.RENDER_ATTACHMENT,
+ });
+ const encoder = t.device.createCommandEncoder();
+ encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: texture.createView(),
+ clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ });
+
+g.test('begin_render_bundle_encoder')
+ .desc(
+ `
+Test that it is valid to begin render bundle encoder with rg11b10ufloat texture
+format iff rg11b10ufloat-renderable feature is enabled.
+`
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase('rg11b10ufloat-renderable');
+ })
+ .fn(async t => {
+ t.device.createRenderBundleEncoder({
+ colorFormats: ['rg11b10ufloat'],
+ });
+ });
+
+g.test('create_render_pipeline')
+ .desc(
+ `
+Test that it is valid to create render pipeline with rg11b10ufloat texture format
+in descriptor.fragment.targets iff rg11b10ufloat-renderable feature is enabled.
+`
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase('rg11b10ufloat-renderable');
+ })
+ .fn(async t => {
+ t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: t.getNoOpShaderCode('VERTEX'),
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: t.getNoOpShaderCode('FRAGMENT'),
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rg11b10ufloat', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts
new file mode 100644
index 0000000000..ad6d030251
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts
@@ -0,0 +1,448 @@
+import {
+ ValidBindableResource,
+ BindableResource,
+ kMaxQueryCount,
+ ShaderStageKey,
+} from '../../capability_info.js';
+import { GPUTest, ResourceState } from '../../gpu_test.js';
+
+/**
+ * Base fixture for WebGPU validation tests.
+ */
+export class ValidationTest extends GPUTest {
+ /**
+ * Create a GPUTexture in the specified state.
+ * A `descriptor` may optionally be passed, which is used when `state` is not `'invalid'`.
+ */
+ createTextureWithState(
+ state: ResourceState,
+ descriptor?: Readonly<GPUTextureDescriptor>
+ ): GPUTexture {
+ descriptor = descriptor ?? {
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage:
+ GPUTextureUsage.COPY_SRC |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.STORAGE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ };
+
+ switch (state) {
+ case 'valid':
+ return this.trackForCleanup(this.device.createTexture(descriptor));
+ case 'invalid':
+ return this.getErrorTexture();
+ case 'destroyed': {
+ const texture = this.device.createTexture(descriptor);
+ texture.destroy();
+ return texture;
+ }
+ }
+ }
+
+ /**
+ * Create a GPUTexture in the specified state. A `descriptor` may optionally be passed;
+ * if `state` is `'invalid'`, it will be modified to add an invalid combination of usages.
+ */
+ createBufferWithState(
+ state: ResourceState,
+ descriptor?: Readonly<GPUBufferDescriptor>
+ ): GPUBuffer {
+ descriptor = descriptor ?? {
+ size: 4,
+ usage: GPUBufferUsage.VERTEX,
+ };
+
+ switch (state) {
+ case 'valid':
+ return this.trackForCleanup(this.device.createBuffer(descriptor));
+
+ case 'invalid': {
+ // Make the buffer invalid because of an invalid combination of usages but keep the
+ // descriptor passed as much as possible (for mappedAtCreation and friends).
+ this.device.pushErrorScope('validation');
+ const buffer = this.device.createBuffer({
+ ...descriptor,
+ usage: descriptor.usage | GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_SRC,
+ });
+ void this.device.popErrorScope();
+ return buffer;
+ }
+ case 'destroyed': {
+ const buffer = this.device.createBuffer(descriptor);
+ buffer.destroy();
+ return buffer;
+ }
+ }
+ }
+
+ /**
+ * Create a GPUQuerySet in the specified state.
+ * A `descriptor` may optionally be passed, which is used when `state` is not `'invalid'`.
+ */
+ createQuerySetWithState(
+ state: ResourceState,
+ desc?: Readonly<GPUQuerySetDescriptor>
+ ): GPUQuerySet {
+ const descriptor = { type: 'occlusion' as const, count: 2, ...desc };
+
+ switch (state) {
+ case 'valid':
+ return this.trackForCleanup(this.device.createQuerySet(descriptor));
+ case 'invalid': {
+ // Make the queryset invalid because of the count out of bounds.
+ descriptor.count = kMaxQueryCount + 1;
+ return this.expectGPUError('validation', () => this.device.createQuerySet(descriptor));
+ }
+ case 'destroyed': {
+ const queryset = this.device.createQuerySet(descriptor);
+ queryset.destroy();
+ return queryset;
+ }
+ }
+ }
+
+ /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage. */
+ getStorageBuffer(): GPUBuffer {
+ return this.trackForCleanup(
+ this.device.createBuffer({ size: 1024, usage: GPUBufferUsage.STORAGE })
+ );
+ }
+
+ /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage. */
+ getUniformBuffer(): GPUBuffer {
+ return this.trackForCleanup(
+ this.device.createBuffer({ size: 1024, usage: GPUBufferUsage.UNIFORM })
+ );
+ }
+
+ /** Return an invalid GPUBuffer. */
+ getErrorBuffer(): GPUBuffer {
+ return this.createBufferWithState('invalid');
+ }
+
+ /** Return an invalid GPUSampler. */
+ getErrorSampler(): GPUSampler {
+ this.device.pushErrorScope('validation');
+ const sampler = this.device.createSampler({ lodMinClamp: -1 });
+ void this.device.popErrorScope();
+ return sampler;
+ }
+
+ /**
+ * Return an arbitrarily-configured GPUTexture with the `TEXTURE_BINDING` usage and specified
+ * sampleCount. The `RENDER_ATTACHMENT` usage will also be specified if sampleCount > 1 as is
+ * required by WebGPU SPEC.
+ */
+ getSampledTexture(sampleCount: number = 1): GPUTexture {
+ const usage =
+ sampleCount > 1
+ ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
+ : GPUTextureUsage.TEXTURE_BINDING;
+ return this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage,
+ sampleCount,
+ })
+ );
+ }
+
+ /** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */
+ getStorageTexture(): GPUTexture {
+ return this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.STORAGE_BINDING,
+ })
+ );
+ }
+
+ /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage. */
+ getRenderTexture(sampleCount: number = 1): GPUTexture {
+ return this.trackForCleanup(
+ this.device.createTexture({
+ size: { width: 16, height: 16, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount,
+ })
+ );
+ }
+
+ /** Return an invalid GPUTexture. */
+ getErrorTexture(): GPUTexture {
+ this.device.pushErrorScope('validation');
+ const texture = this.device.createTexture({
+ size: { width: 0, height: 0, depthOrArrayLayers: 0 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+ void this.device.popErrorScope();
+ return texture;
+ }
+
+ /** Return an invalid GPUTextureView (created from an invalid GPUTexture). */
+ getErrorTextureView(): GPUTextureView {
+ this.device.pushErrorScope('validation');
+ const view = this.getErrorTexture().createView();
+ void this.device.popErrorScope();
+ return view;
+ }
+
+ /**
+ * Return an arbitrary object of the specified {@link webgpu/capability_info!BindableResource} type
+ * (e.g. `'errorBuf'`, `'nonFiltSamp'`, `sampledTexMS`, etc.)
+ */
+ getBindingResource(bindingType: BindableResource): GPUBindingResource {
+ switch (bindingType) {
+ case 'errorBuf':
+ return { buffer: this.getErrorBuffer() };
+ case 'errorSamp':
+ return this.getErrorSampler();
+ case 'errorTex':
+ return this.getErrorTextureView();
+ case 'uniformBuf':
+ return { buffer: this.getUniformBuffer() };
+ case 'storageBuf':
+ return { buffer: this.getStorageBuffer() };
+ case 'filtSamp':
+ return this.device.createSampler({ minFilter: 'linear' });
+ case 'nonFiltSamp':
+ return this.device.createSampler();
+ case 'compareSamp':
+ return this.device.createSampler({ compare: 'never' });
+ case 'sampledTex':
+ return this.getSampledTexture(1).createView();
+ case 'sampledTexMS':
+ return this.getSampledTexture(4).createView();
+ case 'storageTex':
+ return this.getStorageTexture().createView();
+ }
+ }
+
+ /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage from mismatched device. */
+ getDeviceMismatchedStorageBuffer(): GPUBuffer {
+ return this.trackForCleanup(
+ this.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE })
+ );
+ }
+
+ /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage from mismatched device. */
+ getDeviceMismatchedUniformBuffer(): GPUBuffer {
+ return this.trackForCleanup(
+ this.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM })
+ );
+ }
+
+ /** Return a GPUTexture with descriptor from mismatched device. */
+ getDeviceMismatchedTexture(descriptor: GPUTextureDescriptor): GPUTexture {
+ return this.trackForCleanup(this.mismatchedDevice.createTexture(descriptor));
+ }
+
+ /** Return an arbitrarily-configured GPUTexture with the `SAMPLED` usage from mismatched device. */
+ getDeviceMismatchedSampledTexture(sampleCount: number = 1): GPUTexture {
+ return this.getDeviceMismatchedTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ sampleCount,
+ });
+ }
+
+ /** Return an arbitrarily-configured GPUTexture with the `STORAGE` usage from mismatched device. */
+ getDeviceMismatchedStorageTexture(): GPUTexture {
+ return this.getDeviceMismatchedTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.STORAGE_BINDING,
+ });
+ }
+
+ /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage from mismatched device. */
+ getDeviceMismatchedRenderTexture(sampleCount: number = 1): GPUTexture {
+ return this.getDeviceMismatchedTexture({
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount,
+ });
+ }
+
+ getDeviceMismatchedBindingResource(bindingType: ValidBindableResource): GPUBindingResource {
+ switch (bindingType) {
+ case 'uniformBuf':
+ return { buffer: this.getDeviceMismatchedStorageBuffer() };
+ case 'storageBuf':
+ return { buffer: this.getDeviceMismatchedUniformBuffer() };
+ case 'filtSamp':
+ return this.mismatchedDevice.createSampler({ minFilter: 'linear' });
+ case 'nonFiltSamp':
+ return this.mismatchedDevice.createSampler();
+ case 'compareSamp':
+ return this.mismatchedDevice.createSampler({ compare: 'never' });
+ case 'sampledTex':
+ return this.getDeviceMismatchedSampledTexture(1).createView();
+ case 'sampledTexMS':
+ return this.getDeviceMismatchedSampledTexture(4).createView();
+ case 'storageTex':
+ return this.getDeviceMismatchedStorageTexture().createView();
+ }
+ }
+
+ /** Return a no-op shader code snippet for the specified shader stage. */
+ getNoOpShaderCode(stage: ShaderStageKey): string {
+ switch (stage) {
+ case 'VERTEX':
+ return `
+ @vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+ }
+ `;
+ case 'FRAGMENT':
+ return `@fragment fn main() {}`;
+ case 'COMPUTE':
+ return `@compute @workgroup_size(1) fn main() {}`;
+ }
+ }
+
+ /** Create a GPURenderPipeline in the specified state. */
+ createRenderPipelineWithState(state: 'valid' | 'invalid'): GPURenderPipeline {
+ return state === 'valid' ? this.createNoOpRenderPipeline() : this.createErrorRenderPipeline();
+ }
+
+ /** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */
+ createNoOpRenderPipeline(
+ layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto'
+ ): GPURenderPipeline {
+ return this.device.createRenderPipeline({
+ layout,
+ vertex: {
+ module: this.device.createShaderModule({
+ code: this.getNoOpShaderCode('VERTEX'),
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: this.getNoOpShaderCode('FRAGMENT'),
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm', writeMask: 0 }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+ }
+
+ /** Return an invalid GPURenderPipeline. */
+ createErrorRenderPipeline(): GPURenderPipeline {
+ this.device.pushErrorScope('validation');
+ const pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: '',
+ }),
+ entryPoint: '',
+ },
+ });
+ void this.device.popErrorScope();
+ return pipeline;
+ }
+
+ /** Return a GPUComputePipeline with a no-op shader. */
+ createNoOpComputePipeline(
+ layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto'
+ ): GPUComputePipeline {
+ return this.device.createComputePipeline({
+ layout,
+ compute: {
+ module: this.device.createShaderModule({
+ code: this.getNoOpShaderCode('COMPUTE'),
+ }),
+ entryPoint: 'main',
+ },
+ });
+ }
+
+ /** Return an invalid GPUComputePipeline. */
+ createErrorComputePipeline(): GPUComputePipeline {
+ this.device.pushErrorScope('validation');
+ const pipeline = this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({
+ code: '',
+ }),
+ entryPoint: '',
+ },
+ });
+ void this.device.popErrorScope();
+ return pipeline;
+ }
+
+ /** Return an invalid GPUShaderModule. */
+ createInvalidShaderModule(): GPUShaderModule {
+ this.device.pushErrorScope('validation');
+ const code = 'deadbeaf'; // Something make no sense
+ const shaderModule = this.device.createShaderModule({ code });
+ void this.device.popErrorScope();
+ return shaderModule;
+ }
+
+ /** Helper for testing createRenderPipeline(Async) validation */
+ doCreateRenderPipelineTest(
+ isAsync: boolean,
+ _success: boolean,
+ descriptor: GPURenderPipelineDescriptor,
+ errorTypeName: 'OperationError' | 'TypeError' = 'OperationError'
+ ) {
+ if (isAsync) {
+ if (_success) {
+ this.shouldResolve(this.device.createRenderPipelineAsync(descriptor));
+ } else {
+ this.shouldReject(errorTypeName, this.device.createRenderPipelineAsync(descriptor));
+ }
+ } else {
+ if (errorTypeName === 'OperationError') {
+ this.expectValidationError(() => {
+ this.device.createRenderPipeline(descriptor);
+ }, !_success);
+ } else {
+ this.shouldThrow(_success ? false : errorTypeName, () => {
+ this.device.createRenderPipeline(descriptor);
+ });
+ }
+ }
+ }
+
+ /** Helper for testing createComputePipeline(Async) validation */
+ doCreateComputePipelineTest(
+ isAsync: boolean,
+ _success: boolean,
+ descriptor: GPUComputePipelineDescriptor,
+ errorTypeName: 'OperationError' | 'TypeError' = 'OperationError'
+ ) {
+ if (isAsync) {
+ if (_success) {
+ this.shouldResolve(this.device.createComputePipelineAsync(descriptor));
+ } else {
+ this.shouldReject(errorTypeName, this.device.createComputePipelineAsync(descriptor));
+ }
+ } else {
+ if (errorTypeName === 'OperationError') {
+ this.expectValidationError(() => {
+ this.device.createComputePipeline(descriptor);
+ }, !_success);
+ } else {
+ this.shouldThrow(_success ? false : errorTypeName, () => {
+ this.device.createComputePipeline(descriptor);
+ });
+ }
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts
new file mode 100644
index 0000000000..4981328ce3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/capability_info.ts
@@ -0,0 +1,1123 @@
+// MAINTENANCE_TODO: The generated Typedoc for this file is hard to navigate because it's
+// alphabetized. Consider using namespaces or renames to fix this?
+
+/* eslint-disable no-sparse-arrays */
+
+import { keysOf, makeTable, numericKeysOf, valueof } from '../common/util/data_tables.js';
+import { assertTypeTrue, TypeEqual } from '../common/util/types.js';
+import { assert, unreachable } from '../common/util/util.js';
+
+import { GPUConst, kMaxUnsignedLongValue, kMaxUnsignedLongLongValue } from './constants.js';
+import { ImageCopyType } from './util/texture/layout.js';
+
+// Base device limits can be found in constants.ts.
+
+// Queries
+
+/** Maximum number of queries in GPUQuerySet, by spec. */
+export const kMaxQueryCount = 4096;
+/** Per-GPUQueryType info. */
+export type QueryTypeInfo = {
+ /** Optional feature required to use this GPUQueryType. */
+ readonly feature: GPUFeatureName | undefined;
+ // Add fields as needed
+};
+export const kQueryTypeInfo: {
+ readonly [k in GPUQueryType]: QueryTypeInfo;
+} = /* prettier-ignore */ {
+ // Occlusion query does not require any features.
+ 'occlusion': { feature: undefined },
+ 'timestamp': { feature: 'timestamp-query' },
+};
+/** List of all GPUQueryType values. */
+export const kQueryTypes = keysOf(kQueryTypeInfo);
+
+// Buffers
+
+/** Required alignment of a GPUBuffer size, by spec. */
+export const kBufferSizeAlignment = 4;
+
+/** Per-GPUBufferUsage copy info. */
+export const kBufferUsageCopyInfo: {
+ readonly [name: string]: GPUBufferUsageFlags;
+} = /* prettier-ignore */ {
+ 'COPY_NONE': 0,
+ 'COPY_SRC': GPUConst.BufferUsage.COPY_SRC,
+ 'COPY_DST': GPUConst.BufferUsage.COPY_DST,
+ 'COPY_SRC_DST': GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.COPY_DST,
+};
+/** List of all GPUBufferUsage copy values. */
+export const kBufferUsageCopy = keysOf(kBufferUsageCopyInfo);
+
+/** Per-GPUBufferUsage keys and info. */
+type BufferUsageKey = keyof typeof GPUConst.BufferUsage;
+export const kBufferUsageKeys = keysOf(GPUConst.BufferUsage);
+export const kBufferUsageInfo: {
+ readonly [k in BufferUsageKey]: GPUBufferUsageFlags;
+} = {
+ ...GPUConst.BufferUsage,
+};
+
+/** List of all GPUBufferUsage values. */
+export const kBufferUsages = Object.values(GPUConst.BufferUsage);
+export const kAllBufferUsageBits = kBufferUsages.reduce(
+ (previousSet, currentUsage) => previousSet | currentUsage,
+ 0
+);
+
+// Errors
+
+/** Per-GPUErrorFilter info. */
+export const kErrorScopeFilterInfo: {
+ readonly [k in GPUErrorFilter]: {};
+} = /* prettier-ignore */ {
+ 'out-of-memory': {},
+ 'validation': {},
+ 'internal': {},
+};
+/** List of all GPUErrorFilter values. */
+export const kErrorScopeFilters = keysOf(kErrorScopeFilterInfo);
+export const kGeneratableErrorScopeFilters = kErrorScopeFilters.filter(e => e !== 'internal');
+
+// Textures
+
+// Definitions for use locally. To access the table entries, use `kTextureFormatInfo`.
+
+// Note that we repeat the header multiple times in order to make it easier to read.
+const kRegularTextureFormatInfo = /* prettier-ignore */ makeTable(
+ ['renderable', 'multisample', 'resolve', 'color', 'depth', 'stencil', 'storage', 'copySrc', 'copyDst', 'sampleType', 'bytesPerBlock', 'blockWidth', 'blockHeight', 'renderTargetPixelByteCost', 'renderTargetComponentAlignment', 'feature', 'baseFormat'] as const,
+ [ , , , true, false, false, , true, true, , , 1, 1, undefined, undefined, , undefined] as const, {
+ // 8-bit formats
+ 'r8unorm': [ true, true, true, , , , false, , , 'float', 1, , , 1, 1],
+ 'r8snorm': [ false, false, false, , , , false, , , 'float', 1],
+ 'r8uint': [ true, true, false, , , , false, , , 'uint', 1, , , 1, 1],
+ 'r8sint': [ true, true, false, , , , false, , , 'sint', 1, , , 1, 1],
+ // 16-bit formats
+ 'r16uint': [ true, true, false, , , , false, , , 'uint', 2, , , 2, 2],
+ 'r16sint': [ true, true, false, , , , false, , , 'sint', 2, , , 2, 2],
+ 'r16float': [ true, true, true, , , , false, , , 'float', 2, , , 2, 2],
+ 'rg8unorm': [ true, true, true, , , , false, , , 'float', 2, , , 2, 1],
+ 'rg8snorm': [ false, false, false, , , , false, , , 'float', 2],
+ 'rg8uint': [ true, true, false, , , , false, , , 'uint', 2, , , 2, 1],
+ 'rg8sint': [ true, true, false, , , , false, , , 'sint', 2, , , 2, 1],
+ // 32-bit formats
+ 'r32uint': [ true, false, false, , , , true, , , 'uint', 4, , , 4, 4],
+ 'r32sint': [ true, false, false, , , , true, , , 'sint', 4, , , 4, 4],
+ 'r32float': [ true, true, false, , , , true, , , 'unfilterable-float', 4, , , 4, 4],
+ 'rg16uint': [ true, true, false, , , , false, , , 'uint', 4, , , 4, 2],
+ 'rg16sint': [ true, true, false, , , , false, , , 'sint', 4, , , 4, 2],
+ 'rg16float': [ true, true, true, , , , false, , , 'float', 4, , , 4, 2],
+ 'rgba8unorm': [ true, true, true, , , , true, , , 'float', 4, , , 8, 1, , 'rgba8unorm'],
+ 'rgba8unorm-srgb': [ true, true, true, , , , false, , , 'float', 4, , , 8, 1, , 'rgba8unorm'],
+ 'rgba8snorm': [ false, false, false, , , , true, , , 'float', 4],
+ 'rgba8uint': [ true, true, false, , , , true, , , 'uint', 4, , , 4, 1],
+ 'rgba8sint': [ true, true, false, , , , true, , , 'sint', 4, , , 4, 1],
+ 'bgra8unorm': [ true, true, true, , , , false, , , 'float', 4, , , 8, 1, , 'bgra8unorm'],
+ 'bgra8unorm-srgb': [ true, true, true, , , , false, , , 'float', 4, , , 8, 1, , 'bgra8unorm'],
+ // Packed 32-bit formats
+ 'rgb10a2unorm': [ true, true, true, , , , false, , , 'float', 4, , , 8, 4],
+ 'rg11b10ufloat': [ false, false, false, , , , false, , , 'float', 4, , , 8, 4],
+ 'rgb9e5ufloat': [ false, false, false, , , , false, , , 'float', 4],
+ // 64-bit formats
+ 'rg32uint': [ true, false, false, , , , true, , , 'uint', 8, , , 8, 4],
+ 'rg32sint': [ true, false, false, , , , true, , , 'sint', 8, , , 8, 4],
+ 'rg32float': [ true, false, false, , , , true, , , 'unfilterable-float', 8, , , 8, 4],
+ 'rgba16uint': [ true, true, false, , , , true, , , 'uint', 8, , , 8, 2],
+ 'rgba16sint': [ true, true, false, , , , true, , , 'sint', 8, , , 8, 2],
+ 'rgba16float': [ true, true, true, , , , true, , , 'float', 8, , , 8, 2],
+ // 128-bit formats
+ 'rgba32uint': [ true, false, false, , , , true, , , 'uint', 16, , , 16, 4],
+ 'rgba32sint': [ true, false, false, , , , true, , , 'sint', 16, , , 16, 4],
+ 'rgba32float': [ true, false, false, , , , true, , , 'unfilterable-float', 16, , , 16, 4],
+} as const);
+/* prettier-ignore */
+const kTexFmtInfoHeader = ['renderable', 'multisample', 'resolve', 'color', 'depth', 'stencil', 'storage', 'copySrc', 'copyDst', 'sampleType', 'bytesPerBlock', 'blockWidth', 'blockHeight', 'renderTargetPixelByteCost', 'renderTargetComponentAlignment', 'feature', 'baseFormat'] as const;
+const kSizedDepthStencilFormatInfo = /* prettier-ignore */ makeTable(kTexFmtInfoHeader,
+ [ true, true, false, false, , , false, , , , , 1, 1, undefined, undefined, , undefined] as const, {
+ 'depth32float': [ , , , , true, false, , true, false, 'depth', 4],
+ 'depth16unorm': [ , , , , true, false, , true, true, 'depth', 2],
+ 'stencil8': [ , , , , false, true, , true, true, 'uint', 1],
+} as const);
+
+// Multi aspect sample type are now set to their first aspect
+const kUnsizedDepthStencilFormatInfo = /* prettier-ignore */ makeTable(kTexFmtInfoHeader,
+ [ true, true, false, false, , , false, false, false, , undefined, 1, 1, , , , undefined] as const, {
+ 'depth24plus': [ , , , , true, false, , , , 'depth'],
+ 'depth24plus-stencil8': [ , , , , true, true, , , , 'depth'],
+ // MAINTENANCE_TODO: These should really be sized formats; see below MAINTENANCE_TODO about multi-aspect formats.
+ 'depth32float-stencil8': [ , , , , true, true, , , , 'depth', , , , , , 'depth32float-stencil8'],
+} as const);
+
+// Separated compressed formats by type
+const kBCTextureFormatInfo = /* prettier-ignore */ makeTable(kTexFmtInfoHeader,
+ [ false, false, false, true, false, false, false, true, true, , , 4, 4, , , , undefined] as const, {
+ // Block Compression (BC) formats
+ 'bc1-rgba-unorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-bc', 'bc1-rgba-unorm'],
+ 'bc1-rgba-unorm-srgb': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-bc', 'bc1-rgba-unorm'],
+ 'bc2-rgba-unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc2-rgba-unorm'],
+ 'bc2-rgba-unorm-srgb': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc2-rgba-unorm'],
+ 'bc3-rgba-unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc3-rgba-unorm'],
+ 'bc3-rgba-unorm-srgb': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc3-rgba-unorm'],
+ 'bc4-r-unorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-bc'],
+ 'bc4-r-snorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-bc'],
+ 'bc5-rg-unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc'],
+ 'bc5-rg-snorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc'],
+ 'bc6h-rgb-ufloat': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc'],
+ 'bc6h-rgb-float': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc'],
+ 'bc7-rgba-unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc7-rgba-unorm'],
+ 'bc7-rgba-unorm-srgb': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-bc', 'bc7-rgba-unorm'],
+} as const);
+const kETC2TextureFormatInfo = /* prettier-ignore */ makeTable(kTexFmtInfoHeader,
+ [ false, false, false, true, false, false, false, true, true, , , 4, 4, , , , undefined] as const, {
+ // Ericsson Compression (ETC2) formats
+ 'etc2-rgb8unorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2', 'etc2-rgb8unorm'],
+ 'etc2-rgb8unorm-srgb': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2', 'etc2-rgb8unorm'],
+ 'etc2-rgb8a1unorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2', 'etc2-rgb8a1unorm'],
+ 'etc2-rgb8a1unorm-srgb': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2', 'etc2-rgb8a1unorm'],
+ 'etc2-rgba8unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-etc2', 'etc2-rgba8unorm'],
+ 'etc2-rgba8unorm-srgb': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-etc2', 'etc2-rgba8unorm'],
+ 'eac-r11unorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2'],
+ 'eac-r11snorm': [ , , , , , , , , , 'float', 8, 4, 4, , , 'texture-compression-etc2'],
+ 'eac-rg11unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-etc2'],
+ 'eac-rg11snorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-etc2'],
+} as const);
+const kASTCTextureFormatInfo = /* prettier-ignore */ makeTable(kTexFmtInfoHeader,
+ [ false, false, false, true, false, false, false, true, true, , , , , , , , undefined] as const, {
+ // Adaptable Scalable Compression (ASTC) formats
+ 'astc-4x4-unorm': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-astc', 'astc-4x4-unorm'],
+ 'astc-4x4-unorm-srgb': [ , , , , , , , , , 'float', 16, 4, 4, , , 'texture-compression-astc', 'astc-4x4-unorm'],
+ 'astc-5x4-unorm': [ , , , , , , , , , 'float', 16, 5, 4, , , 'texture-compression-astc', 'astc-5x4-unorm'],
+ 'astc-5x4-unorm-srgb': [ , , , , , , , , , 'float', 16, 5, 4, , , 'texture-compression-astc', 'astc-5x4-unorm'],
+ 'astc-5x5-unorm': [ , , , , , , , , , 'float', 16, 5, 5, , , 'texture-compression-astc', 'astc-5x5-unorm'],
+ 'astc-5x5-unorm-srgb': [ , , , , , , , , , 'float', 16, 5, 5, , , 'texture-compression-astc', 'astc-5x5-unorm'],
+ 'astc-6x5-unorm': [ , , , , , , , , , 'float', 16, 6, 5, , , 'texture-compression-astc', 'astc-6x5-unorm'],
+ 'astc-6x5-unorm-srgb': [ , , , , , , , , , 'float', 16, 6, 5, , , 'texture-compression-astc', 'astc-6x5-unorm'],
+ 'astc-6x6-unorm': [ , , , , , , , , , 'float', 16, 6, 6, , , 'texture-compression-astc', 'astc-6x6-unorm'],
+ 'astc-6x6-unorm-srgb': [ , , , , , , , , , 'float', 16, 6, 6, , , 'texture-compression-astc', 'astc-6x6-unorm'],
+ 'astc-8x5-unorm': [ , , , , , , , , , 'float', 16, 8, 5, , , 'texture-compression-astc', 'astc-8x5-unorm'],
+ 'astc-8x5-unorm-srgb': [ , , , , , , , , , 'float', 16, 8, 5, , , 'texture-compression-astc', 'astc-8x5-unorm'],
+ 'astc-8x6-unorm': [ , , , , , , , , , 'float', 16, 8, 6, , , 'texture-compression-astc', 'astc-8x6-unorm'],
+ 'astc-8x6-unorm-srgb': [ , , , , , , , , , 'float', 16, 8, 6, , , 'texture-compression-astc', 'astc-8x6-unorm'],
+ 'astc-8x8-unorm': [ , , , , , , , , , 'float', 16, 8, 8, , , 'texture-compression-astc', 'astc-8x8-unorm'],
+ 'astc-8x8-unorm-srgb': [ , , , , , , , , , 'float', 16, 8, 8, , , 'texture-compression-astc', 'astc-8x8-unorm'],
+ 'astc-10x5-unorm': [ , , , , , , , , , 'float', 16, 10, 5, , , 'texture-compression-astc', 'astc-10x5-unorm'],
+ 'astc-10x5-unorm-srgb': [ , , , , , , , , , 'float', 16, 10, 5, , , 'texture-compression-astc', 'astc-10x5-unorm'],
+ 'astc-10x6-unorm': [ , , , , , , , , , 'float', 16, 10, 6, , , 'texture-compression-astc', 'astc-10x6-unorm'],
+ 'astc-10x6-unorm-srgb': [ , , , , , , , , , 'float', 16, 10, 6, , , 'texture-compression-astc', 'astc-10x6-unorm'],
+ 'astc-10x8-unorm': [ , , , , , , , , , 'float', 16, 10, 8, , , 'texture-compression-astc', 'astc-10x8-unorm'],
+ 'astc-10x8-unorm-srgb': [ , , , , , , , , , 'float', 16, 10, 8, , , 'texture-compression-astc', 'astc-10x8-unorm'],
+ 'astc-10x10-unorm': [ , , , , , , , , , 'float', 16, 10, 10, , , 'texture-compression-astc', 'astc-10x10-unorm'],
+ 'astc-10x10-unorm-srgb': [ , , , , , , , , , 'float', 16, 10, 10, , , 'texture-compression-astc', 'astc-10x10-unorm'],
+ 'astc-12x10-unorm': [ , , , , , , , , , 'float', 16, 12, 10, , , 'texture-compression-astc', 'astc-12x10-unorm'],
+ 'astc-12x10-unorm-srgb': [ , , , , , , , , , 'float', 16, 12, 10, , , 'texture-compression-astc', 'astc-12x10-unorm'],
+ 'astc-12x12-unorm': [ , , , , , , , , , 'float', 16, 12, 12, , , 'texture-compression-astc', 'astc-12x12-unorm'],
+ 'astc-12x12-unorm-srgb': [ , , , , , , , , , 'float', 16, 12, 12, , , 'texture-compression-astc', 'astc-12x12-unorm'],
+} as const);
+
+// Definitions for use locally. To access the table entries, use `kTextureFormatInfo`.
+
+// MAINTENANCE_TODO: Consider generating the exports below programmatically by filtering the big list, instead
+// of using these local constants? Requires some type magic though.
+/* prettier-ignore */ const kCompressedTextureFormatInfo = { ...kBCTextureFormatInfo, ...kETC2TextureFormatInfo, ...kASTCTextureFormatInfo } as const;
+/* prettier-ignore */ const kColorTextureFormatInfo = { ...kRegularTextureFormatInfo, ...kCompressedTextureFormatInfo } as const;
+/* prettier-ignore */ const kEncodableTextureFormatInfo = { ...kRegularTextureFormatInfo, ...kSizedDepthStencilFormatInfo } as const;
+/* prettier-ignore */ const kSizedTextureFormatInfo = { ...kRegularTextureFormatInfo, ...kSizedDepthStencilFormatInfo, ...kCompressedTextureFormatInfo } as const;
+/* prettier-ignore */ const kDepthStencilFormatInfo = { ...kSizedDepthStencilFormatInfo, ...kUnsizedDepthStencilFormatInfo } as const;
+/* prettier-ignore */ const kUncompressedTextureFormatInfo = { ...kRegularTextureFormatInfo, ...kSizedDepthStencilFormatInfo, ...kUnsizedDepthStencilFormatInfo } as const;
+/* prettier-ignore */ const kAllTextureFormatInfo = { ...kUncompressedTextureFormatInfo, ...kCompressedTextureFormatInfo } as const;
+
+/** A "regular" texture format (uncompressed, sized, single-plane color formats). */
+/* prettier-ignore */ export type RegularTextureFormat = keyof typeof kRegularTextureFormatInfo;
+/** A sized depth/stencil texture format. */
+/* prettier-ignore */ export type SizedDepthStencilFormat = keyof typeof kSizedDepthStencilFormatInfo;
+/** An unsized depth/stencil texture format. */
+/* prettier-ignore */ export type UnsizedDepthStencilFormat = keyof typeof kUnsizedDepthStencilFormatInfo;
+/** A compressed (block) texture format. */
+/* prettier-ignore */ export type CompressedTextureFormat = keyof typeof kCompressedTextureFormatInfo;
+
+/** A color texture format (regular | compressed). */
+/* prettier-ignore */ export type ColorTextureFormat = keyof typeof kColorTextureFormatInfo;
+/** An encodable texture format (regular | sized depth/stencil). */
+/* prettier-ignore */ export type EncodableTextureFormat = keyof typeof kEncodableTextureFormatInfo;
+/** A sized texture format (regular | sized depth/stencil | compressed). */
+/* prettier-ignore */ export type SizedTextureFormat = keyof typeof kSizedTextureFormatInfo;
+/** A depth/stencil format (sized | unsized). */
+/* prettier-ignore */ export type DepthStencilFormat = keyof typeof kDepthStencilFormatInfo;
+/** An uncompressed (block size 1x1) format (regular | depth/stencil). */
+/* prettier-ignore */ export type UncompressedTextureFormat = keyof typeof kUncompressedTextureFormatInfo;
+
+/* prettier-ignore */ export const kRegularTextureFormats: readonly RegularTextureFormat[] = keysOf( kRegularTextureFormatInfo);
+/* prettier-ignore */ export const kSizedDepthStencilFormats: readonly SizedDepthStencilFormat[] = keysOf( kSizedDepthStencilFormatInfo);
+/* prettier-ignore */ export const kUnsizedDepthStencilFormats: readonly UnsizedDepthStencilFormat[] = keysOf(kUnsizedDepthStencilFormatInfo);
+/* prettier-ignore */ export const kCompressedTextureFormats: readonly CompressedTextureFormat[] = keysOf( kCompressedTextureFormatInfo);
+
+/* prettier-ignore */ export const kColorTextureFormats: readonly ColorTextureFormat[] = keysOf( kColorTextureFormatInfo);
+/* prettier-ignore */ export const kEncodableTextureFormats: readonly EncodableTextureFormat[] = keysOf( kEncodableTextureFormatInfo);
+/* prettier-ignore */ export const kSizedTextureFormats: readonly SizedTextureFormat[] = keysOf( kSizedTextureFormatInfo);
+/* prettier-ignore */ export const kDepthStencilFormats: readonly DepthStencilFormat[] = keysOf( kDepthStencilFormatInfo);
+/* prettier-ignore */ export const kUncompressedTextureFormats: readonly UncompressedTextureFormat[] = keysOf(kUncompressedTextureFormatInfo);
+/* prettier-ignore */ export const kAllTextureFormats: readonly GPUTextureFormat[] = keysOf( kAllTextureFormatInfo);
+
+// CompressedTextureFormat are unrenderable so filter from RegularTextureFormats for color targets is enough
+export const kRenderableColorTextureFormats = kRegularTextureFormats.filter(
+ v => kColorTextureFormatInfo[v].renderable
+);
+assert(
+ kRenderableColorTextureFormats.every(
+ f =>
+ kAllTextureFormatInfo[f].renderTargetComponentAlignment !== undefined &&
+ kAllTextureFormatInfo[f].renderTargetPixelByteCost !== undefined
+ )
+);
+
+// The formats of GPUTextureFormat for canvas context.
+export const kCanvasTextureFormats = ['bgra8unorm', 'rgba8unorm', 'rgba16float'] as const;
+
+// The alpha mode for canvas context.
+export const kCanvasAlphaModesInfo: {
+ readonly [k in GPUCanvasAlphaMode]: {};
+} = /* prettier-ignore */ {
+ 'opaque': {},
+ 'premultiplied': {},
+};
+export const kCanvasAlphaModes = keysOf(kCanvasAlphaModesInfo);
+
+// The color spaces for canvas context
+export const kCanvasColorSpacesInfo: {
+ readonly [k in PredefinedColorSpace]: {};
+} = /* prettier-ignore */ {
+ 'srgb': {},
+ 'display-p3': {},
+};
+export const kCanvasColorSpaces = keysOf(kCanvasColorSpacesInfo);
+
+/** Per-GPUTextureFormat info. */
+// Exists just for documentation. Otherwise could be inferred by `makeTable`.
+// MAINTENANCE_TODO: Refactor this to separate per-aspect data for multi-aspect formats. In particular:
+// - bytesPerBlock only makes sense on a per-aspect basis. But this table can't express that.
+// So we put depth32float-stencil8 to be an unsized format for now.
+export type TextureFormatInfo = {
+ /** Whether the format can be used as `RENDER_ATTACHMENT`. */
+ renderable: boolean;
+ /** Whether the format can be used in a multisample texture. */
+ multisample: boolean;
+ /** Whether the texture with the format can be used as a resolve target. */
+ resolve: boolean;
+ /** Whether the format has a color aspect. */
+ color: boolean;
+ /** Whether the format has a depth aspect. */
+ depth: boolean;
+ /** Whether the format has a stencil aspect. */
+ stencil: boolean;
+ /** Whether the format can be used as `STORAGE`. */
+ storage: boolean;
+ /** Whether the format can be used as `COPY_SRC`. */
+ copySrc: boolean;
+ /** Whether the format can be used as `COPY_DST`. */
+ copyDst: boolean;
+ /** Byte size of one texel block, or `undefined` if the format is unsized. */
+ bytesPerBlock: number | undefined;
+ /** Width, in texels, of one texel block. */
+ blockWidth: number;
+ /** Height, in texels, of one texel block. */
+ blockHeight: number;
+ /** The raw, unaligned, byte cost towards the color attachment bytes per sample.
+ * (See https://www.w3.org/TR/webgpu/#abstract-opdef-calculating-color-attachment-bytes-per-sample). */
+ renderTargetPixelByteCost: number | undefined;
+ /** The alignment used for the format when computing the color attachment bytes per sample. */
+ renderTargetComponentAlignment: number | undefined;
+ /** Optional feature required to use this format, or `undefined` if none. */
+ feature: GPUFeatureName | undefined;
+ // Add fields as needed
+};
+/** Per-GPUTextureFormat info. */
+export const kTextureFormatInfo: {
+ readonly [k in GPUTextureFormat]: TextureFormatInfo &
+ // TextureFormatInfo exists just for documentation (and verification of the table data types).
+ // The next line constrains the types so that accessing kTextureFormatInfo with
+ // a subtype of GPUTextureFormat actually returns nicely a constrained info type
+ // (e.g. with `bytesPerBlock: number` instead of `bytesPerBlock: number | undefined`).
+ typeof kAllTextureFormatInfo[k];
+} = kAllTextureFormatInfo;
+/** List of all GPUTextureFormat values. */
+/* prettier-ignore */ export const kTextureFormats: readonly GPUTextureFormat[] = keysOf(kAllTextureFormatInfo);
+
+/** Valid GPUTextureFormats for `copyExternalImageToTexture`, by spec. */
+export const kValidTextureFormatsForCopyE2T = [
+ 'r8unorm',
+ 'r16float',
+ 'r32float',
+ 'rg8unorm',
+ 'rg16float',
+ 'rg32float',
+ 'rgba8unorm',
+ 'rgba8unorm-srgb',
+ 'bgra8unorm',
+ 'bgra8unorm-srgb',
+ 'rgb10a2unorm',
+ 'rgba16float',
+ 'rgba32float',
+] as const;
+
+/** Per-GPUTextureDimension info. */
+export const kTextureDimensionInfo: {
+ readonly [k in GPUTextureDimension]: {};
+} = /* prettier-ignore */ {
+ '1d': {},
+ '2d': {},
+ '3d': {},
+};
+/** List of all GPUTextureDimension values. */
+export const kTextureDimensions = keysOf(kTextureDimensionInfo);
+
+/** Per-GPUTextureAspect info. */
+export const kTextureAspectInfo: {
+ readonly [k in GPUTextureAspect]: {};
+} = /* prettier-ignore */ {
+ 'all': {},
+ 'depth-only': {},
+ 'stencil-only': {},
+};
+/** List of all GPUTextureAspect values. */
+export const kTextureAspects = keysOf(kTextureAspectInfo);
+
+/** Per-GPUCompareFunction info. */
+export const kCompareFunctionInfo: {
+ readonly [k in GPUCompareFunction]: {};
+} = /* prettier-ignore */ {
+ 'never': {},
+ 'less': {},
+ 'equal': {},
+ 'less-equal': {},
+ 'greater': {},
+ 'not-equal': {},
+ 'greater-equal': {},
+ 'always': {},
+};
+/** List of all GPUCompareFunction values. */
+export const kCompareFunctions = keysOf(kCompareFunctionInfo);
+
+/** Per-GPUStencilOperation info. */
+export const kStencilOperationInfo: {
+ readonly [k in GPUStencilOperation]: {};
+} = /* prettier-ignore */ {
+ 'keep': {},
+ 'zero': {},
+ 'replace': {},
+ 'invert': {},
+ 'increment-clamp': {},
+ 'decrement-clamp': {},
+ 'increment-wrap': {},
+ 'decrement-wrap': {},
+};
+/** List of all GPUStencilOperation values. */
+export const kStencilOperations = keysOf(kStencilOperationInfo);
+
+const kDepthStencilFormatCapabilityInBufferTextureCopy = {
+ // kUnsizedDepthStencilFormats
+ depth24plus: {
+ CopyB2T: [],
+ CopyT2B: [],
+ texelAspectSize: { 'depth-only': -1, 'stencil-only': -1 },
+ },
+ 'depth24plus-stencil8': {
+ CopyB2T: ['stencil-only'],
+ CopyT2B: ['stencil-only'],
+ texelAspectSize: { 'depth-only': -1, 'stencil-only': 1 },
+ },
+
+ // kSizedDepthStencilFormats
+ depth16unorm: {
+ CopyB2T: ['all', 'depth-only'],
+ CopyT2B: ['all', 'depth-only'],
+ texelAspectSize: { 'depth-only': 2, 'stencil-only': -1 },
+ },
+ depth32float: {
+ CopyB2T: [],
+ CopyT2B: ['all', 'depth-only'],
+ texelAspectSize: { 'depth-only': 4, 'stencil-only': -1 },
+ },
+ 'depth32float-stencil8': {
+ CopyB2T: ['stencil-only'],
+ CopyT2B: ['depth-only', 'stencil-only'],
+ texelAspectSize: { 'depth-only': 4, 'stencil-only': 1 },
+ },
+ stencil8: {
+ CopyB2T: ['all', 'stencil-only'],
+ CopyT2B: ['all', 'stencil-only'],
+ texelAspectSize: { 'depth-only': -1, 'stencil-only': 1 },
+ },
+} as const;
+
+/** `kDepthStencilFormatResolvedAspect[format][aspect]` returns the aspect-specific format for a
+ * depth-stencil format, or `undefined` if the format doesn't have the aspect.
+ */
+export const kDepthStencilFormatResolvedAspect: {
+ readonly [k in DepthStencilFormat]: {
+ readonly [a in GPUTextureAspect]: DepthStencilFormat | undefined;
+ };
+} = {
+ // kUnsizedDepthStencilFormats
+ depth24plus: {
+ all: 'depth24plus',
+ 'depth-only': 'depth24plus',
+ 'stencil-only': undefined,
+ },
+ 'depth24plus-stencil8': {
+ all: 'depth24plus-stencil8',
+ 'depth-only': 'depth24plus',
+ 'stencil-only': 'stencil8',
+ },
+
+ // kSizedDepthStencilFormats
+ depth16unorm: {
+ all: 'depth16unorm',
+ 'depth-only': 'depth16unorm',
+ 'stencil-only': undefined,
+ },
+ depth32float: {
+ all: 'depth32float',
+ 'depth-only': 'depth32float',
+ 'stencil-only': undefined,
+ },
+ 'depth32float-stencil8': {
+ all: 'depth32float-stencil8',
+ 'depth-only': 'depth32float',
+ 'stencil-only': 'stencil8',
+ },
+ stencil8: {
+ all: 'stencil8',
+ 'depth-only': undefined,
+ 'stencil-only': 'stencil8',
+ },
+} as const;
+
+/**
+ * @returns the GPUTextureFormat corresponding to the @param aspect of @param format.
+ * This allows choosing the correct format for depth-stencil aspects when creating pipelines that
+ * will have to match the resolved format of views, or to get per-aspect information like the
+ * `blockByteSize`.
+ *
+ * Many helpers use an `undefined` `aspect` to means `'all'` so this is also the default for this
+ * function.
+ */
+export function resolvePerAspectFormat(
+ format: GPUTextureFormat,
+ aspect?: GPUTextureAspect
+): GPUTextureFormat {
+ if (aspect === 'all' || aspect === undefined) {
+ return format;
+ }
+ assert(kTextureFormatInfo[format].depth || kTextureFormatInfo[format].stencil);
+ const resolved = kDepthStencilFormatResolvedAspect[format as DepthStencilFormat][aspect ?? 'all'];
+ assert(resolved !== undefined);
+ return resolved;
+}
+
+/**
+ * Gets all copyable aspects for copies between texture and buffer for specified depth/stencil format and copy type, by spec.
+ */
+export function depthStencilFormatCopyableAspects(
+ type: ImageCopyType,
+ format: DepthStencilFormat
+): readonly GPUTextureAspect[] {
+ const appliedType = type === 'WriteTexture' ? 'CopyB2T' : type;
+ return kDepthStencilFormatCapabilityInBufferTextureCopy[format][appliedType];
+}
+
+/**
+ * Computes whether a copy between a depth/stencil texture aspect and a buffer is supported, by spec.
+ */
+export function depthStencilBufferTextureCopySupported(
+ type: ImageCopyType,
+ format: DepthStencilFormat,
+ aspect: GPUTextureAspect
+): boolean {
+ const supportedAspects: readonly GPUTextureAspect[] = depthStencilFormatCopyableAspects(
+ type,
+ format
+ );
+ return supportedAspects.includes(aspect);
+}
+
+/**
+ * Returns the byte size of the depth or stencil aspect of the specified depth/stencil format,
+ * or -1 if none.
+ */
+export function depthStencilFormatAspectSize(
+ format: DepthStencilFormat,
+ aspect: 'depth-only' | 'stencil-only'
+) {
+ const texelAspectSize =
+ kDepthStencilFormatCapabilityInBufferTextureCopy[format].texelAspectSize[aspect];
+ assert(texelAspectSize > 0);
+ return texelAspectSize;
+}
+
+/**
+ * Returns true iff a texture can be created with the provided GPUTextureDimension
+ * (defaulting to 2d) and GPUTextureFormat, by spec.
+ */
+export function textureDimensionAndFormatCompatible(
+ dimension: undefined | GPUTextureDimension,
+ format: GPUTextureFormat
+): boolean {
+ const info = kAllTextureFormatInfo[format];
+ return !(
+ (dimension === '1d' || dimension === '3d') &&
+ (info.blockWidth > 1 || info.depth || info.stencil)
+ );
+}
+
+/** Per-GPUTextureUsage type info. */
+export const kTextureUsageTypeInfo: {
+ readonly [name: string]: number;
+} = /* prettier-ignore */ {
+ 'texture': Number(GPUConst.TextureUsage.TEXTURE_BINDING),
+ 'storage': Number(GPUConst.TextureUsage.STORAGE_BINDING),
+ 'render': Number(GPUConst.TextureUsage.RENDER_ATTACHMENT),
+};
+/** List of all GPUTextureUsage type values. */
+export const kTextureUsageType = keysOf(kTextureUsageTypeInfo);
+
+/** Per-GPUTextureUsage copy info. */
+export const kTextureUsageCopyInfo: {
+ readonly [name: string]: number;
+} = /* prettier-ignore */ {
+ 'none': 0,
+ 'src': Number(GPUConst.TextureUsage.COPY_SRC),
+ 'dst': Number(GPUConst.TextureUsage.COPY_DST),
+ 'src-dest': Number(GPUConst.TextureUsage.COPY_SRC) | Number(GPUConst.TextureUsage.COPY_DST),
+};
+/** List of all GPUTextureUsage copy values. */
+export const kTextureUsageCopy = keysOf(kTextureUsageCopyInfo);
+
+/** Per-GPUTextureUsage info. */
+export const kTextureUsageInfo: {
+ readonly [k in valueof<typeof GPUConst.TextureUsage>]: {};
+} = {
+ [GPUConst.TextureUsage.COPY_SRC]: {},
+ [GPUConst.TextureUsage.COPY_DST]: {},
+ [GPUConst.TextureUsage.TEXTURE_BINDING]: {},
+ [GPUConst.TextureUsage.STORAGE_BINDING]: {},
+ [GPUConst.TextureUsage.RENDER_ATTACHMENT]: {},
+};
+/** List of all GPUTextureUsage values. */
+export const kTextureUsages = numericKeysOf<GPUTextureUsageFlags>(kTextureUsageInfo);
+
+// Texture View
+
+/** Per-GPUTextureViewDimension info. */
+export type TextureViewDimensionInfo = {
+ /** Whether a storage texture view can have this view dimension. */
+ readonly storage: boolean;
+ // Add fields as needed
+};
+/** Per-GPUTextureViewDimension info. */
+export const kTextureViewDimensionInfo: {
+ readonly [k in GPUTextureViewDimension]: TextureViewDimensionInfo;
+} = /* prettier-ignore */ {
+ '1d': { storage: true },
+ '2d': { storage: true },
+ '2d-array': { storage: true },
+ 'cube': { storage: false },
+ 'cube-array': { storage: false },
+ '3d': { storage: true },
+};
+/** List of all GPUTextureDimension values. */
+export const kTextureViewDimensions = keysOf(kTextureViewDimensionInfo);
+
+// Vertex formats
+
+/** Per-GPUVertexFormat info. */
+// Exists just for documentation. Otherwise could be inferred by `makeTable`.
+export type VertexFormatInfo = {
+ /** Number of bytes in each component. */
+ readonly bytesPerComponent: 1 | 2 | 4;
+ /** The data encoding (float, normalized, or integer) for each component. */
+ readonly type: 'float' | 'unorm' | 'snorm' | 'uint' | 'sint';
+ /** Number of components. */
+ readonly componentCount: 1 | 2 | 3 | 4;
+ /** The completely matching WGSL type for vertex format */
+ readonly wgslType:
+ | 'f32'
+ | 'vec2<f32>'
+ | 'vec3<f32>'
+ | 'vec4<f32>'
+ | 'u32'
+ | 'vec2<u32>'
+ | 'vec3<u32>'
+ | 'vec4<u32>'
+ | 'i32'
+ | 'vec2<i32>'
+ | 'vec3<i32>'
+ | 'vec4<i32>';
+ // Add fields as needed
+};
+/** Per-GPUVertexFormat info. */
+export const kVertexFormatInfo: {
+ readonly [k in GPUVertexFormat]: VertexFormatInfo;
+} = /* prettier-ignore */ makeTable(
+ ['bytesPerComponent', 'type', 'componentCount', 'wgslType'] as const,
+ [ , , , ] as const, {
+ // 8 bit components
+ 'uint8x2': [ 1, 'uint', 2, 'vec2<u32>'],
+ 'uint8x4': [ 1, 'uint', 4, 'vec4<u32>'],
+ 'sint8x2': [ 1, 'sint', 2, 'vec2<i32>'],
+ 'sint8x4': [ 1, 'sint', 4, 'vec4<i32>'],
+ 'unorm8x2': [ 1, 'unorm', 2, 'vec2<f32>'],
+ 'unorm8x4': [ 1, 'unorm', 4, 'vec4<f32>'],
+ 'snorm8x2': [ 1, 'snorm', 2, 'vec2<f32>'],
+ 'snorm8x4': [ 1, 'snorm', 4, 'vec4<f32>'],
+ // 16 bit components
+ 'uint16x2': [ 2, 'uint', 2, 'vec2<u32>'],
+ 'uint16x4': [ 2, 'uint', 4, 'vec4<u32>'],
+ 'sint16x2': [ 2, 'sint', 2, 'vec2<i32>'],
+ 'sint16x4': [ 2, 'sint', 4, 'vec4<i32>'],
+ 'unorm16x2': [ 2, 'unorm', 2, 'vec2<f32>'],
+ 'unorm16x4': [ 2, 'unorm', 4, 'vec4<f32>'],
+ 'snorm16x2': [ 2, 'snorm', 2, 'vec2<f32>'],
+ 'snorm16x4': [ 2, 'snorm', 4, 'vec4<f32>'],
+ 'float16x2': [ 2, 'float', 2, 'vec2<f32>'],
+ 'float16x4': [ 2, 'float', 4, 'vec4<f32>'],
+ // 32 bit components
+ 'float32': [ 4, 'float', 1, 'f32'],
+ 'float32x2': [ 4, 'float', 2, 'vec2<f32>'],
+ 'float32x3': [ 4, 'float', 3, 'vec3<f32>'],
+ 'float32x4': [ 4, 'float', 4, 'vec4<f32>'],
+ 'uint32': [ 4, 'uint', 1, 'u32'],
+ 'uint32x2': [ 4, 'uint', 2, 'vec2<u32>'],
+ 'uint32x3': [ 4, 'uint', 3, 'vec3<u32>'],
+ 'uint32x4': [ 4, 'uint', 4, 'vec4<u32>'],
+ 'sint32': [ 4, 'sint', 1, 'i32'],
+ 'sint32x2': [ 4, 'sint', 2, 'vec2<i32>'],
+ 'sint32x3': [ 4, 'sint', 3, 'vec3<i32>'],
+ 'sint32x4': [ 4, 'sint', 4, 'vec4<i32>']
+} as const);
+/** List of all GPUVertexFormat values. */
+export const kVertexFormats = keysOf(kVertexFormatInfo);
+
+// Typedefs for bindings
+
+/**
+ * Classes of `PerShaderStage` binding limits. Two bindings with the same class
+ * count toward the same `PerShaderStage` limit(s) in the spec (if any).
+ */
+export type PerStageBindingLimitClass =
+ | 'uniformBuf'
+ | 'storageBuf'
+ | 'sampler'
+ | 'sampledTex'
+ | 'storageTex';
+/**
+ * Classes of `PerPipelineLayout` binding limits. Two bindings with the same class
+ * count toward the same `PerPipelineLayout` limit(s) in the spec (if any).
+ */
+export type PerPipelineBindingLimitClass = PerStageBindingLimitClass;
+
+export type ValidBindableResource =
+ | 'uniformBuf'
+ | 'storageBuf'
+ | 'filtSamp'
+ | 'nonFiltSamp'
+ | 'compareSamp'
+ | 'sampledTex'
+ | 'sampledTexMS'
+ | 'storageTex';
+type ErrorBindableResource = 'errorBuf' | 'errorSamp' | 'errorTex';
+
+/**
+ * Types of resource binding which have distinct binding rules, by spec
+ * (e.g. filtering vs non-filtering sampler, multisample vs non-multisample texture).
+ */
+export type BindableResource = ValidBindableResource | ErrorBindableResource;
+export const kBindableResources = [
+ 'uniformBuf',
+ 'storageBuf',
+ 'filtSamp',
+ 'nonFiltSamp',
+ 'compareSamp',
+ 'sampledTex',
+ 'sampledTexMS',
+ 'storageTex',
+ 'errorBuf',
+ 'errorSamp',
+ 'errorTex',
+] as const;
+assertTypeTrue<TypeEqual<BindableResource, typeof kBindableResources[number]>>();
+
+// Bindings
+
+/** Dynamic buffer offsets require offset to be divisible by 256, by spec. */
+export const kMinDynamicBufferOffsetAlignment = 256;
+
+/** Default `PerShaderStage` binding limits, by spec. */
+export const kPerStageBindingLimits: {
+ readonly [k in PerStageBindingLimitClass]: {
+ /** Which `PerShaderStage` binding limit class. */
+ readonly class: k;
+ /** Maximum number of allowed bindings in that class. */
+ readonly max: number;
+ // Add fields as needed
+ };
+} = /* prettier-ignore */ {
+ 'uniformBuf': { class: 'uniformBuf', max: 12, },
+ 'storageBuf': { class: 'storageBuf', max: 8, },
+ 'sampler': { class: 'sampler', max: 16, },
+ 'sampledTex': { class: 'sampledTex', max: 16, },
+ 'storageTex': { class: 'storageTex', max: 4, },
+};
+
+/**
+ * Default `PerPipelineLayout` binding limits, by spec.
+ */
+export const kPerPipelineBindingLimits: {
+ readonly [k in PerPipelineBindingLimitClass]: {
+ /** Which `PerPipelineLayout` binding limit class. */
+ readonly class: k;
+ /** Maximum number of allowed bindings with `hasDynamicOffset: true` in that class. */
+ readonly maxDynamic: number;
+ // Add fields as needed
+ };
+} = /* prettier-ignore */ {
+ 'uniformBuf': { class: 'uniformBuf', maxDynamic: 8, },
+ 'storageBuf': { class: 'storageBuf', maxDynamic: 4, },
+ 'sampler': { class: 'sampler', maxDynamic: 0, },
+ 'sampledTex': { class: 'sampledTex', maxDynamic: 0, },
+ 'storageTex': { class: 'storageTex', maxDynamic: 0, },
+};
+
+interface BindingKindInfo {
+ readonly resource: ValidBindableResource;
+ readonly perStageLimitClass: typeof kPerStageBindingLimits[PerStageBindingLimitClass];
+ readonly perPipelineLimitClass: typeof kPerPipelineBindingLimits[PerPipelineBindingLimitClass];
+ // Add fields as needed
+}
+
+const kBindingKind: {
+ readonly [k in ValidBindableResource]: BindingKindInfo;
+} = /* prettier-ignore */ {
+ uniformBuf: { resource: 'uniformBuf', perStageLimitClass: kPerStageBindingLimits.uniformBuf, perPipelineLimitClass: kPerPipelineBindingLimits.uniformBuf, },
+ storageBuf: { resource: 'storageBuf', perStageLimitClass: kPerStageBindingLimits.storageBuf, perPipelineLimitClass: kPerPipelineBindingLimits.storageBuf, },
+ filtSamp: { resource: 'filtSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, },
+ nonFiltSamp: { resource: 'nonFiltSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, },
+ compareSamp: { resource: 'compareSamp', perStageLimitClass: kPerStageBindingLimits.sampler, perPipelineLimitClass: kPerPipelineBindingLimits.sampler, },
+ sampledTex: { resource: 'sampledTex', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
+ sampledTexMS: { resource: 'sampledTexMS', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
+ storageTex: { resource: 'storageTex', perStageLimitClass: kPerStageBindingLimits.storageTex, perPipelineLimitClass: kPerPipelineBindingLimits.storageTex, },
+};
+
+// Binding type info
+
+const kValidStagesAll = {
+ validStages:
+ GPUConst.ShaderStage.VERTEX | GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE,
+} as const;
+const kValidStagesStorageWrite = {
+ validStages: GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE,
+} as const;
+
+/** Binding type info (including class limits) for the specified GPUBufferBindingLayout. */
+export function bufferBindingTypeInfo(d: GPUBufferBindingLayout) {
+ /* prettier-ignore */
+ switch (d.type ?? 'uniform') {
+ case 'uniform': return { usage: GPUConst.BufferUsage.UNIFORM, ...kBindingKind.uniformBuf, ...kValidStagesAll, };
+ case 'storage': return { usage: GPUConst.BufferUsage.STORAGE, ...kBindingKind.storageBuf, ...kValidStagesStorageWrite, };
+ case 'read-only-storage': return { usage: GPUConst.BufferUsage.STORAGE, ...kBindingKind.storageBuf, ...kValidStagesAll, };
+ }
+}
+/** List of all GPUBufferBindingType values. */
+export const kBufferBindingTypes = ['uniform', 'storage', 'read-only-storage'] as const;
+assertTypeTrue<TypeEqual<GPUBufferBindingType, typeof kBufferBindingTypes[number]>>();
+
+/** Binding type info (including class limits) for the specified GPUSamplerBindingLayout. */
+export function samplerBindingTypeInfo(d: GPUSamplerBindingLayout) {
+ /* prettier-ignore */
+ switch (d.type ?? 'filtering') {
+ case 'filtering': return { ...kBindingKind.filtSamp, ...kValidStagesAll, };
+ case 'non-filtering': return { ...kBindingKind.nonFiltSamp, ...kValidStagesAll, };
+ case 'comparison': return { ...kBindingKind.compareSamp, ...kValidStagesAll, };
+ }
+}
+/** List of all GPUSamplerBindingType values. */
+export const kSamplerBindingTypes = ['filtering', 'non-filtering', 'comparison'] as const;
+assertTypeTrue<TypeEqual<GPUSamplerBindingType, typeof kSamplerBindingTypes[number]>>();
+
+/** Binding type info (including class limits) for the specified GPUTextureBindingLayout. */
+export function sampledTextureBindingTypeInfo(d: GPUTextureBindingLayout) {
+ /* prettier-ignore */
+ if (d.multisampled) {
+ return { usage: GPUConst.TextureUsage.TEXTURE_BINDING, ...kBindingKind.sampledTexMS, ...kValidStagesAll, };
+ } else {
+ return { usage: GPUConst.TextureUsage.TEXTURE_BINDING, ...kBindingKind.sampledTex, ...kValidStagesAll, };
+ }
+}
+/** List of all GPUTextureSampleType values. */
+export const kTextureSampleTypes = [
+ 'float',
+ 'unfilterable-float',
+ 'depth',
+ 'sint',
+ 'uint',
+] as const;
+assertTypeTrue<TypeEqual<GPUTextureSampleType, typeof kTextureSampleTypes[number]>>();
+
+/** Binding type info (including class limits) for the specified GPUStorageTextureBindingLayout. */
+export function storageTextureBindingTypeInfo(d: GPUStorageTextureBindingLayout) {
+ return {
+ usage: GPUConst.TextureUsage.STORAGE_BINDING,
+ ...kBindingKind.storageTex,
+ ...kValidStagesStorageWrite,
+ };
+}
+/** List of all GPUStorageTextureAccess values. */
+export const kStorageTextureAccessValues = ['write-only'] as const;
+assertTypeTrue<TypeEqual<GPUStorageTextureAccess, typeof kStorageTextureAccessValues[number]>>();
+
+/** GPUBindGroupLayoutEntry, but only the "union" fields, not the common fields. */
+export type BGLEntry = Omit<GPUBindGroupLayoutEntry, 'binding' | 'visibility'>;
+/** Binding type info (including class limits) for the specified BGLEntry. */
+export function texBindingTypeInfo(e: BGLEntry) {
+ if (e.texture !== undefined) return sampledTextureBindingTypeInfo(e.texture);
+ if (e.storageTexture !== undefined) return storageTextureBindingTypeInfo(e.storageTexture);
+ unreachable();
+}
+/** BindingTypeInfo (including class limits) for the specified BGLEntry. */
+export function bindingTypeInfo(e: BGLEntry) {
+ if (e.buffer !== undefined) return bufferBindingTypeInfo(e.buffer);
+ if (e.texture !== undefined) return sampledTextureBindingTypeInfo(e.texture);
+ if (e.sampler !== undefined) return samplerBindingTypeInfo(e.sampler);
+ if (e.storageTexture !== undefined) return storageTextureBindingTypeInfo(e.storageTexture);
+ unreachable('GPUBindGroupLayoutEntry has no BindingLayout');
+}
+
+/**
+ * Generate a list of possible buffer-typed BGLEntry values.
+ *
+ * Note: Generates different `type` options, but not `hasDynamicOffset` options.
+ */
+export function bufferBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
+ return [
+ ...(includeUndefined ? [{ buffer: { type: undefined } }] : []),
+ { buffer: { type: 'uniform' } },
+ { buffer: { type: 'storage' } },
+ { buffer: { type: 'read-only-storage' } },
+ ] as const;
+}
+/** Generate a list of possible sampler-typed BGLEntry values. */
+export function samplerBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
+ return [
+ ...(includeUndefined ? [{ sampler: { type: undefined } }] : []),
+ { sampler: { type: 'comparison' } },
+ { sampler: { type: 'filtering' } },
+ { sampler: { type: 'non-filtering' } },
+ ] as const;
+}
+/**
+ * Generate a list of possible texture-typed BGLEntry values.
+ *
+ * Note: Generates different `multisampled` options, but not `sampleType` or `viewDimension` options.
+ */
+export function textureBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
+ return [
+ ...(includeUndefined ? [{ texture: { multisampled: undefined } }] : []),
+ { texture: { multisampled: false } },
+ { texture: { multisampled: true } },
+ ] as const;
+}
+/**
+ * Generate a list of possible storageTexture-typed BGLEntry values.
+ *
+ * Note: Generates different `access` options, but not `format` or `viewDimension` options.
+ */
+export function storageTextureBindingEntries(format: GPUTextureFormat): readonly BGLEntry[] {
+ return [{ storageTexture: { access: 'write-only', format } }] as const;
+}
+/** Generate a list of possible texture-or-storageTexture-typed BGLEntry values. */
+export function sampledAndStorageBindingEntries(
+ includeUndefined: boolean,
+ storageTextureFormat: GPUTextureFormat = 'rgba8unorm'
+): readonly BGLEntry[] {
+ return [
+ ...textureBindingEntries(includeUndefined),
+ ...storageTextureBindingEntries(storageTextureFormat),
+ ] as const;
+}
+/**
+ * Generate a list of possible BGLEntry values of every type, but not variants with different:
+ * - buffer.hasDynamicOffset
+ * - texture.sampleType
+ * - texture.viewDimension
+ * - storageTexture.viewDimension
+ */
+export function allBindingEntries(
+ includeUndefined: boolean,
+ storageTextureFormat: GPUTextureFormat = 'rgba8unorm'
+): readonly BGLEntry[] {
+ return [
+ ...bufferBindingEntries(includeUndefined),
+ ...samplerBindingEntries(includeUndefined),
+ ...sampledAndStorageBindingEntries(includeUndefined, storageTextureFormat),
+ ] as const;
+}
+
+// Shader stages
+
+/** List of all GPUShaderStage values. */
+export type ShaderStageKey = keyof typeof GPUConst.ShaderStage;
+export const kShaderStageKeys = Object.keys(GPUConst.ShaderStage) as ShaderStageKey[];
+export const kShaderStages: readonly GPUShaderStageFlags[] = [
+ GPUConst.ShaderStage.VERTEX,
+ GPUConst.ShaderStage.FRAGMENT,
+ GPUConst.ShaderStage.COMPUTE,
+];
+/** List of all possible combinations of GPUShaderStage values. */
+export const kShaderStageCombinations: readonly GPUShaderStageFlags[] = [0, 1, 2, 3, 4, 5, 6, 7];
+
+/**
+ * List of all possible texture sampleCount values.
+ *
+ * MAINTENANCE_TODO: Switch existing tests to use kTextureSampleCounts
+ */
+export const kTextureSampleCounts = [1, 4] as const;
+
+// Blend factors and Blend components
+
+/** List of all GPUBlendFactor values. */
+export const kBlendFactors: readonly GPUBlendFactor[] = [
+ 'zero',
+ 'one',
+ 'src',
+ 'one-minus-src',
+ 'src-alpha',
+ 'one-minus-src-alpha',
+ 'dst',
+ 'one-minus-dst',
+ 'dst-alpha',
+ 'one-minus-dst-alpha',
+ 'src-alpha-saturated',
+ 'constant',
+ 'one-minus-constant',
+];
+
+/** List of all GPUBlendOperation values. */
+export const kBlendOperations: readonly GPUBlendOperation[] = [
+ 'add', //
+ 'subtract',
+ 'reverse-subtract',
+ 'min',
+ 'max',
+];
+
+// Primitive topologies
+export const kPrimitiveTopology: readonly GPUPrimitiveTopology[] = [
+ 'point-list',
+ 'line-list',
+ 'line-strip',
+ 'triangle-list',
+ 'triangle-strip',
+];
+assertTypeTrue<TypeEqual<GPUPrimitiveTopology, typeof kPrimitiveTopology[number]>>();
+
+export const kIndexFormat: readonly GPUIndexFormat[] = ['uint16', 'uint32'];
+assertTypeTrue<TypeEqual<GPUIndexFormat, typeof kIndexFormat[number]>>();
+
+/** Info for each entry of GPUSupportedLimits */
+export const kLimitInfo = /* prettier-ignore */ makeTable(
+ [ 'class', 'default', 'maximumValue'] as const,
+ [ 'maximum', , kMaxUnsignedLongValue] as const, {
+ 'maxTextureDimension1D': [ , 8192, ],
+ 'maxTextureDimension2D': [ , 8192, ],
+ 'maxTextureDimension3D': [ , 2048, ],
+ 'maxTextureArrayLayers': [ , 256, ],
+
+ 'maxBindGroups': [ , 4, ],
+ 'maxDynamicUniformBuffersPerPipelineLayout': [ , 8, ],
+ 'maxDynamicStorageBuffersPerPipelineLayout': [ , 4, ],
+ 'maxSampledTexturesPerShaderStage': [ , 16, ],
+ 'maxSamplersPerShaderStage': [ , 16, ],
+ 'maxStorageBuffersPerShaderStage': [ , 8, ],
+ 'maxStorageTexturesPerShaderStage': [ , 4, ],
+ 'maxUniformBuffersPerShaderStage': [ , 12, ],
+
+ 'maxUniformBufferBindingSize': [ , 65536, kMaxUnsignedLongLongValue],
+ 'maxStorageBufferBindingSize': [ , 134217728, kMaxUnsignedLongLongValue],
+ 'minUniformBufferOffsetAlignment': ['alignment', 256, ],
+ 'minStorageBufferOffsetAlignment': ['alignment', 256, ],
+
+ 'maxVertexBuffers': [ , 8, ],
+ 'maxBufferSize': [ , 268435456, kMaxUnsignedLongLongValue],
+ 'maxVertexAttributes': [ , 16, ],
+ 'maxVertexBufferArrayStride': [ , 2048, ],
+ 'maxInterStageShaderComponents': [ , 60, ],
+
+ 'maxColorAttachments': [ , 8, ],
+ 'maxColorAttachmentBytesPerSample': [ , 32, ],
+
+ 'maxComputeWorkgroupStorageSize': [ , 16384, ],
+ 'maxComputeInvocationsPerWorkgroup': [ , 256, ],
+ 'maxComputeWorkgroupSizeX': [ , 256, ],
+ 'maxComputeWorkgroupSizeY': [ , 256, ],
+ 'maxComputeWorkgroupSizeZ': [ , 64, ],
+ 'maxComputeWorkgroupsPerDimension': [ , 65535, ],
+} as const);
+
+/** List of all entries of GPUSupportedLimits. */
+export const kLimits = keysOf(kLimitInfo);
+
+// Pipeline limits
+
+/** Maximum number of color attachments to a render pass, by spec. */
+export const kMaxColorAttachments = kLimitInfo.maxColorAttachments.default;
+/** `maxVertexBuffers` per GPURenderPipeline, by spec. */
+export const kMaxVertexBuffers = kLimitInfo.maxVertexBuffers.default;
+/** `maxVertexAttributes` per GPURenderPipeline, by spec. */
+export const kMaxVertexAttributes = kLimitInfo.maxVertexAttributes.default;
+/** `maxVertexBufferArrayStride` in a vertex buffer in a GPURenderPipeline, by spec. */
+export const kMaxVertexBufferArrayStride = kLimitInfo.maxVertexBufferArrayStride.default;
+
+/** The size of indirect draw parameters in the indirectBuffer of drawIndirect */
+export const kDrawIndirectParametersSize = 4;
+/** The size of indirect drawIndexed parameters in the indirectBuffer of drawIndexedIndirect */
+export const kDrawIndexedIndirectParametersSize = 5;
+
+/** Per-GPUFeatureName info. */
+export const kFeatureNameInfo: {
+ readonly [k in GPUFeatureName]: {};
+} = /* prettier-ignore */ {
+ 'depth-clip-control': {},
+ 'depth32float-stencil8': {},
+ 'texture-compression-bc': {},
+ 'texture-compression-etc2': {},
+ 'texture-compression-astc': {},
+ 'timestamp-query': {},
+ 'indirect-first-instance': {},
+ 'shader-f16': {},
+ 'rg11b10ufloat-renderable': {},
+};
+/** List of all GPUFeatureName values. */
+export const kFeatureNames = keysOf(kFeatureNameInfo);
+
+/**
+ * Check if two formats are view format compatible.
+ *
+ * This function may need to be generalized to use `baseFormat` from `kTextureFormatInfo`.
+ */
+export function viewCompatible(a: GPUTextureFormat, b: GPUTextureFormat): boolean {
+ return a === b || a + '-srgb' === b || b + '-srgb' === a;
+}
+
+export function getFeaturesForFormats<T>(
+ formats: readonly (T & (GPUTextureFormat | undefined))[]
+): readonly (GPUFeatureName | undefined)[] {
+ return Array.from(new Set(formats.map(f => (f ? kTextureFormatInfo[f].feature : undefined))));
+}
+
+export function filterFormatsByFeature<T>(
+ feature: GPUFeatureName | undefined,
+ formats: readonly (T & (GPUTextureFormat | undefined))[]
+): readonly (T & (GPUTextureFormat | undefined))[] {
+ return formats.filter(f => f === undefined || kTextureFormatInfo[f].feature === feature);
+}
+
+export const kFeaturesForFormats = getFeaturesForFormats(kTextureFormats);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts
new file mode 100644
index 0000000000..6a44983b53
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/constants.ts
@@ -0,0 +1,62 @@
+// Note: Types ensure every field is specified.
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function checkType<T>(x: T) {}
+
+const BufferUsage = {
+ MAP_READ: 0x0001,
+ MAP_WRITE: 0x0002,
+ COPY_SRC: 0x0004,
+ COPY_DST: 0x0008,
+ INDEX: 0x0010,
+ VERTEX: 0x0020,
+ UNIFORM: 0x0040,
+ STORAGE: 0x0080,
+ INDIRECT: 0x0100,
+ QUERY_RESOLVE: 0x0200,
+} as const;
+checkType<Omit<GPUBufferUsage, '__brand'>>(BufferUsage);
+
+const TextureUsage = {
+ COPY_SRC: 0x01,
+ COPY_DST: 0x02,
+ TEXTURE_BINDING: 0x04,
+ SAMPLED: 0x04,
+ STORAGE_BINDING: 0x08,
+ STORAGE: 0x08,
+ RENDER_ATTACHMENT: 0x10,
+} as const;
+checkType<Omit<GPUTextureUsage, '__brand'>>(TextureUsage);
+
+const ColorWrite = {
+ RED: 0x1,
+ GREEN: 0x2,
+ BLUE: 0x4,
+ ALPHA: 0x8,
+ ALL: 0xf,
+} as const;
+checkType<Omit<GPUColorWrite, '__brand'>>(ColorWrite);
+
+const ShaderStage = {
+ VERTEX: 0x1,
+ FRAGMENT: 0x2,
+ COMPUTE: 0x4,
+} as const;
+checkType<Omit<GPUShaderStage, '__brand'>>(ShaderStage);
+
+const MapMode = {
+ READ: 0x1,
+ WRITE: 0x2,
+} as const;
+checkType<Omit<GPUMapMode, '__brand'>>(MapMode);
+
+export const GPUConst = {
+ BufferUsage,
+ TextureUsage,
+ ColorWrite,
+ ShaderStage,
+ MapMode,
+} as const;
+
+export const kMaxUnsignedLongValue = 4294967295;
+export const kMaxUnsignedLongLongValue = Number.MAX_SAFE_INTEGER;
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/examples.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/examples.spec.ts
new file mode 100644
index 0000000000..61d18bf7d7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/examples.spec.ts
@@ -0,0 +1,274 @@
+export const description = `
+Examples of writing CTS tests with various features.
+
+Start here when looking for examples of basic framework usage.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+
+import { GPUTest } from './gpu_test.js';
+
+// To run these tests in the standalone runner, run `npm start` then open:
+// - http://localhost:XXXX/standalone/?runnow=1&q=webgpu:examples:*
+// To run in WPT, copy/symlink the out-wpt/ directory as the webgpu/ directory in WPT, then open:
+// - (wpt server url)/webgpu/cts.https.html?q=webgpu:examples:
+//
+// Tests here can be run individually or in groups:
+// - ?q=webgpu:examples:basic,async:
+// - ?q=webgpu:examples:basic,async:*
+// - ?q=webgpu:examples:basic,*
+// - ?q=webgpu:examples:*
+
+export const g = makeTestGroup(GPUTest);
+
+// Note: spaces aren't allowed in test names; use underscores.
+g.test('test_name').fn(t => {});
+
+g.test('not_implemented_yet,without_plan').unimplemented();
+g.test('not_implemented_yet,with_plan')
+ .desc(
+ `
+Plan for this test. What it tests. Summary of how it tests that functionality.
+- Description of cases, by describing parameters {a, b, c}
+- x= more parameters {x, y, z}
+`
+ )
+ .unimplemented();
+
+g.test('basic').fn(t => {
+ t.expect(true);
+ t.expect(true, 'true should be true');
+
+ t.shouldThrow(
+ // The expected '.name' of the thrown error.
+ 'TypeError',
+ // This function is run inline inside shouldThrow, and is expected to throw.
+ () => {
+ throw new TypeError();
+ },
+ // Log message.
+ 'function should throw Error'
+ );
+});
+
+g.test('basic,async').fn(async t => {
+ // shouldReject must be awaited to ensure it can wait for the promise before the test ends.
+ t.shouldReject(
+ // The expected '.name' of the thrown error.
+ 'TypeError',
+ // Promise expected to reject.
+ Promise.reject(new TypeError()),
+ // Log message.
+ 'Promise.reject should reject'
+ );
+
+ // Promise can also be an IIFE.
+ t.shouldReject(
+ 'TypeError',
+ (async () => {
+ throw new TypeError();
+ })(),
+ 'Promise.reject should reject'
+ );
+});
+
+g.test('basic,plain_cases')
+ .desc(
+ `
+A test can be parameterized with a simple array of objects using .paramsSimple([ ... ]).
+Each such instance of the test is a "case".
+
+In this example, the following cases are generated (identified by their "query string"),
+each with just one subcase:
+ - webgpu:examples:basic,cases:x=2;y=2 runs 1 subcase, with t.params set to:
+ - { x: 2, y: 2 }
+ - webgpu:examples:basic,cases:x=-10;y=-10 runs 1 subcase, with t.params set to:
+ - { x: -10, y: -10 }
+ `
+ )
+ .paramsSimple([
+ { x: 2, y: 2 }, //
+ { x: -10, y: -10 },
+ ])
+ .fn(t => {
+ t.expect(t.params.x === t.params.y);
+ });
+
+g.test('basic,plain_cases_private')
+ .desc(
+ `
+Parameters can be public ("x", "y") which means they're part of the case name.
+They can also be private by starting with an underscore ("_result"), which passes
+them into the test but does not make them part of the case name:
+
+In this example, the following cases are generated, each with just one subcase:
+ - webgpu:examples:basic,cases:x=2;y=4 runs 1 subcase, with t.params set to:
+ - { x: 2, y: 4, _result: 6 }
+ - webgpu:examples:basic,cases:x=-10;y=18 runs 1 subcase, with t.params set to:
+ - { x: -10, y: 18, _result: 8 }
+ `
+ )
+ .paramsSimple([
+ { x: 2, y: 4, _result: 6 }, //
+ { x: -10, y: 18, _result: 8 },
+ ])
+ .fn(t => {
+ t.expect(t.params.x + t.params.y === t.params._result);
+ });
+// (note the blank comment above to enforce newlines on autoformat)
+
+g.test('basic,builder_cases')
+ .desc(
+ `
+A "CaseParamsBuilder" or "SubcaseParamsBuilder" can be passed to .params() instead.
+The params builder provides facilities for generating tests combinatorially (by cartesian
+product). For convenience, the "unit" CaseParamsBuilder is passed as an argument ("u" below).
+
+In this example, the following cases are generated, each with just one subcase:
+ - webgpu:examples:basic,cases:x=1,y=1 runs 1 subcase, with t.params set to:
+ - { x: 1, y: 1 }
+ - webgpu:examples:basic,cases:x=1,y=2 runs 1 subcase, with t.params set to:
+ - { x: 1, y: 2 }
+ - webgpu:examples:basic,cases:x=2,y=1 runs 1 subcase, with t.params set to:
+ - { x: 2, y: 1 }
+ - webgpu:examples:basic,cases:x=2,y=2 runs 1 subcase, with t.params set to:
+ - { x: 2, y: 2 }
+ `
+ )
+ .params(u =>
+ u //
+ .combineWithParams([{ x: 1 }, { x: 2 }])
+ .combineWithParams([{ y: 1 }, { y: 2 }])
+ )
+ .fn(() => {});
+
+g.test('basic,builder_cases_subcases')
+ .desc(
+ `
+Each case sub-parameterized using .beginSubcases().
+Each such instance of the test is a "subcase", which cannot be run independently of other
+subcases. It is somewhat like wrapping the entire fn body in a for-loop.
+
+In this example, the following cases are generated:
+ - webgpu:examples:basic,cases:x=1 runs 2 subcases, with t.params set to:
+ - { x: 1, y: 1 }
+ - { x: 1, y: 2 }
+ - webgpu:examples:basic,cases:x=2 runs 2 subcases, with t.params set to:
+ - { x: 2, y: 1 }
+ - { x: 2, y: 2 }
+ `
+ )
+ .params(u =>
+ u //
+ .combineWithParams([{ x: 1 }, { x: 2 }])
+ .beginSubcases()
+ .combineWithParams([{ y: 1 }, { y: 2 }])
+ )
+ .fn(() => {});
+
+g.test('basic,builder_subcases')
+ .desc(
+ `
+In this example, the following single case is generated:
+ - webgpu:examples:basic,cases: runs 4 subcases, with t.params set to:
+ - { x: 1, y: 1 }
+ - { x: 1, y: 2 }
+ - { x: 2, y: 1 }
+ - { x: 2, y: 2 }
+ `
+ )
+ .params(u =>
+ u //
+ .beginSubcases()
+ .combineWithParams([{ x: 1 }, { x: 2 }])
+ .combineWithParams([{ y: 1 }, { y: 2 }])
+ )
+ .fn(() => {});
+
+g.test('basic,builder_subcases_short')
+ .desc(
+ `
+As a shorthand, .paramsSubcasesOnly() can be used.
+
+In this example, the following single case is generated:
+ - webgpu:examples:basic,cases: runs 4 subcases, with t.params set to:
+ - { x: 1, y: 1 }
+ - { x: 1, y: 2 }
+ - { x: 2, y: 1 }
+ - { x: 2, y: 2 }
+ `
+ )
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([{ x: 1 }, { x: 2 }])
+ .combineWithParams([{ y: 1 }, { y: 2 }])
+ )
+ .fn(() => {});
+
+g.test('gpu,async').fn(async t => {
+ const x = await t.queue.onSubmittedWorkDone();
+ t.expect(x === undefined);
+});
+
+g.test('gpu,buffers').fn(async t => {
+ const data = new Uint32Array([0, 1234, 0]);
+ const src = t.makeBufferWithContents(data, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST);
+
+ // Use the expectGPUBufferValuesEqual helper to check the actual contents of a GPUBuffer.
+ // This makes a copy and then asynchronously checks the contents. The test fixture will
+ // wait on that result before reporting whether the test passed or failed.
+ t.expectGPUBufferValuesEqual(src, data);
+});
+
+// One of the following two tests should be skipped on most platforms.
+
+g.test('gpu,with_texture_compression,bc')
+ .desc(
+ `Example of a test using a device descriptor.
+Tests that a BC format passes validation iff the feature is enabled.`
+ )
+ .params(u => u.combine('textureCompressionBC', [false, true]))
+ .beforeAllSubcases(t => {
+ const { textureCompressionBC } = t.params;
+
+ if (textureCompressionBC) {
+ t.selectDeviceOrSkipTestCase('texture-compression-bc');
+ }
+ })
+ .fn(async t => {
+ const { textureCompressionBC } = t.params;
+ const shouldError = !textureCompressionBC;
+ t.shouldThrow(shouldError ? 'TypeError' : false, () => {
+ t.device.createTexture({
+ format: 'bc1-rgba-unorm',
+ size: [4, 4, 1],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+ });
+ });
+
+g.test('gpu,with_texture_compression,etc2')
+ .desc(
+ `Example of a test using a device descriptor.
+Tests that an ETC2 format passes validation iff the feature is enabled.`
+ )
+ .params(u => u.combine('textureCompressionETC2', [false, true]))
+ .beforeAllSubcases(t => {
+ const { textureCompressionETC2 } = t.params;
+
+ if (textureCompressionETC2) {
+ t.selectDeviceOrSkipTestCase('texture-compression-etc2' as GPUFeatureName);
+ }
+ })
+ .fn(async t => {
+ const { textureCompressionETC2 } = t.params;
+
+ const shouldError = !textureCompressionETC2;
+ t.shouldThrow(shouldError ? 'TypeError' : false, () => {
+ t.device.createTexture({
+ format: 'etc2-rgb8unorm',
+ size: [4, 4, 1],
+ usage: GPUTextureUsage.TEXTURE_BINDING,
+ });
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts
new file mode 100644
index 0000000000..d9ca169df1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/gpu_test.ts
@@ -0,0 +1,1067 @@
+import { Fixture, SubcaseBatchState, TestParams } from '../common/framework/fixture.js';
+import {
+ assert,
+ range,
+ TypedArrayBufferView,
+ TypedArrayBufferViewConstructor,
+ unreachable,
+} from '../common/util/util.js';
+
+import {
+ EncodableTextureFormat,
+ SizedTextureFormat,
+ kTextureFormatInfo,
+ kQueryTypeInfo,
+ resolvePerAspectFormat,
+} from './capability_info.js';
+import { makeBufferWithContents } from './util/buffer.js';
+import {
+ checkElementsEqual,
+ checkElementsBetween,
+ checkElementsFloat16Between,
+} from './util/check_contents.js';
+import { CommandBufferMaker, EncoderType } from './util/command_buffer_maker.js';
+import { ScalarType } from './util/conversion.js';
+import { DevicePool, DeviceProvider, UncanonicalizedDeviceDescriptor } from './util/device_pool.js';
+import { align, roundDown } from './util/math.js';
+import { makeTextureWithContents } from './util/texture.js';
+import {
+ getTextureCopyLayout,
+ getTextureSubCopyLayout,
+ LayoutOptions as TextureLayoutOptions,
+} from './util/texture/layout.js';
+import { PerTexelComponent, kTexelRepresentationInfo } from './util/texture/texel_data.js';
+import { TexelView } from './util/texture/texel_view.js';
+
+const devicePool = new DevicePool();
+
+// MAINTENANCE_TODO: When DevicePool becomes able to provide multiple devices at once, use the
+// usual one instead of a new one.
+const mismatchedDevicePool = new DevicePool();
+
+const kResourceStateValues = ['valid', 'invalid', 'destroyed'] as const;
+export type ResourceState = typeof kResourceStateValues[number];
+export const kResourceStates: readonly ResourceState[] = kResourceStateValues;
+
+/** Various "convenient" shorthands for GPUDeviceDescriptors for selectDevice functions. */
+type DeviceSelectionDescriptor =
+ | UncanonicalizedDeviceDescriptor
+ | GPUFeatureName
+ | undefined
+ | Array<GPUFeatureName | undefined>;
+
+export function initUncanonicalizedDeviceDescriptor(
+ descriptor: DeviceSelectionDescriptor
+): UncanonicalizedDeviceDescriptor | undefined {
+ if (typeof descriptor === 'string') {
+ return { requiredFeatures: [descriptor] };
+ } else if (descriptor instanceof Array) {
+ return {
+ requiredFeatures: descriptor.filter(f => f !== undefined) as GPUFeatureName[],
+ };
+ } else {
+ return descriptor;
+ }
+}
+
+export class GPUTestSubcaseBatchState extends SubcaseBatchState {
+ /** Provider for default device. */
+ private provider: Promise<DeviceProvider> | undefined;
+ /** Provider for mismatched device. */
+ private mismatchedProvider: Promise<DeviceProvider> | undefined;
+
+ async postInit(): Promise<void> {
+ // Skip all subcases if there's no device.
+ await this.acquireProvider();
+ }
+
+ async finalize(): Promise<void> {
+ await super.finalize();
+
+ // Ensure devicePool.release is called for both providers even if one rejects.
+ await Promise.all([
+ this.provider?.then(x => devicePool.release(x)),
+ this.mismatchedProvider?.then(x => devicePool.release(x)),
+ ]);
+ }
+
+ /** @internal MAINTENANCE_TODO: Make this not visible to test code? */
+ acquireProvider(): Promise<DeviceProvider> {
+ if (this.provider === undefined) {
+ this.selectDeviceOrSkipTestCase(undefined);
+ }
+ assert(this.provider !== undefined);
+ return this.provider;
+ }
+
+ /**
+ * Some tests or cases need particular feature flags or limits to be enabled.
+ * Call this function with a descriptor or feature name (or `undefined`) to select a
+ * GPUDevice with matching capabilities. If this isn't called, a default device is provided.
+ *
+ * If the request isn't supported, throws a SkipTestCase exception to skip the entire test case.
+ */
+ selectDeviceOrSkipTestCase(descriptor: DeviceSelectionDescriptor): void {
+ assert(this.provider === undefined, "Can't selectDeviceOrSkipTestCase() multiple times");
+ this.provider = devicePool.acquire(initUncanonicalizedDeviceDescriptor(descriptor));
+ // Suppress uncaught promise rejection (we'll catch it later).
+ this.provider.catch(() => {});
+ }
+
+ /**
+ * Convenience function for {@link selectDeviceOrSkipTestCase}.
+ * Select a device with the features required by these texture format(s).
+ * If the device creation fails, then skip the test case.
+ */
+ selectDeviceForTextureFormatOrSkipTestCase(
+ formats: GPUTextureFormat | undefined | (GPUTextureFormat | undefined)[]
+ ): void {
+ if (!Array.isArray(formats)) {
+ formats = [formats];
+ }
+ const features = new Set<GPUFeatureName | undefined>();
+ for (const format of formats) {
+ if (format !== undefined) {
+ features.add(kTextureFormatInfo[format].feature);
+ }
+ }
+
+ this.selectDeviceOrSkipTestCase(Array.from(features));
+ }
+
+ /**
+ * Convenience function for {@link selectDeviceOrSkipTestCase}.
+ * Select a device with the features required by these query type(s).
+ * If the device creation fails, then skip the test case.
+ */
+ selectDeviceForQueryTypeOrSkipTestCase(types: GPUQueryType | GPUQueryType[]): void {
+ if (!Array.isArray(types)) {
+ types = [types];
+ }
+ const features = types.map(t => kQueryTypeInfo[t].feature);
+ this.selectDeviceOrSkipTestCase(features);
+ }
+
+ /** @internal MAINTENANCE_TODO: Make this not visible to test code? */
+ acquireMismatchedProvider(): Promise<DeviceProvider> | undefined {
+ return this.mismatchedProvider;
+ }
+
+ /**
+ * Some tests need a second device which is different from the first.
+ * This requests a second device so it will be available during the test. If it is not called,
+ * no second device will be available.
+ *
+ * If the request isn't supported, throws a SkipTestCase exception to skip the entire test case.
+ */
+ selectMismatchedDeviceOrSkipTestCase(descriptor: DeviceSelectionDescriptor): void {
+ assert(
+ this.mismatchedProvider === undefined,
+ "Can't selectMismatchedDeviceOrSkipTestCase() multiple times"
+ );
+
+ this.mismatchedProvider = mismatchedDevicePool.acquire(
+ initUncanonicalizedDeviceDescriptor(descriptor)
+ );
+ // Suppress uncaught promise rejection (we'll catch it later).
+ this.mismatchedProvider.catch(() => {});
+ }
+}
+
+/**
+ * Base fixture for WebGPU tests.
+ */
+export class GPUTest extends Fixture<GPUTestSubcaseBatchState> {
+ public static MakeSharedState(params: TestParams): GPUTestSubcaseBatchState {
+ return new GPUTestSubcaseBatchState(params);
+ }
+
+ // Should never be undefined in a test. If it is, init() must not have run/finished.
+ private provider: DeviceProvider | undefined;
+ private mismatchedProvider: DeviceProvider | undefined;
+
+ async init() {
+ await super.init();
+
+ this.provider = await this.sharedState.acquireProvider();
+ this.mismatchedProvider = await this.sharedState.acquireMismatchedProvider();
+ }
+
+ /**
+ * GPUDevice for the test to use.
+ */
+ get device(): GPUDevice {
+ assert(this.provider !== undefined, 'internal error: GPUDevice missing?');
+ return this.provider.device;
+ }
+
+ /**
+ * GPUDevice for tests requiring a second device different from the default one,
+ * e.g. for creating objects for by device_mismatch validation tests.
+ */
+ get mismatchedDevice(): GPUDevice {
+ assert(
+ this.mismatchedProvider !== undefined,
+ 'selectMismatchedDeviceOrSkipTestCase was not called in beforeAllSubcases'
+ );
+ return this.mismatchedProvider.device;
+ }
+
+ /** GPUQueue for the test to use. (Same as `t.device.queue`.) */
+ get queue(): GPUQueue {
+ return this.device.queue;
+ }
+
+ /** Snapshot a GPUBuffer's contents, returning a new GPUBuffer with the `MAP_READ` usage. */
+ private createCopyForMapRead(src: GPUBuffer, srcOffset: number, size: number): GPUBuffer {
+ assert(srcOffset % 4 === 0);
+ assert(size % 4 === 0);
+
+ const dst = this.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(dst);
+
+ const c = this.device.createCommandEncoder();
+ c.copyBufferToBuffer(src, srcOffset, dst, 0, size);
+ this.queue.submit([c.finish()]);
+
+ return dst;
+ }
+
+ /**
+ * Offset and size passed to createCopyForMapRead must be divisible by 4. For that
+ * we might need to copy more bytes from the buffer than we want to map.
+ * begin and end values represent the part of the copied buffer that stores the contents
+ * we initially wanted to map.
+ * The copy will not cause an OOB error because the buffer size must be 4-aligned.
+ */
+ private createAlignedCopyForMapRead(
+ src: GPUBuffer,
+ size: number,
+ offset: number
+ ): { mappable: GPUBuffer; subarrayByteStart: number } {
+ const alignedOffset = roundDown(offset, 4);
+ const subarrayByteStart = offset - alignedOffset;
+ const alignedSize = align(size + subarrayByteStart, 4);
+ const mappable = this.createCopyForMapRead(src, alignedOffset, alignedSize);
+ return { mappable, subarrayByteStart };
+ }
+
+ /**
+ * Snapshot the current contents of a range of a GPUBuffer, and return them as a TypedArray.
+ * Also provides a cleanup() function to unmap and destroy the staging buffer.
+ */
+ async readGPUBufferRangeTyped<T extends TypedArrayBufferView>(
+ src: GPUBuffer,
+ {
+ srcByteOffset = 0,
+ method = 'copy',
+ type,
+ typedLength,
+ }: {
+ srcByteOffset?: number;
+ method?: 'copy' | 'map';
+ type: TypedArrayBufferViewConstructor<T>;
+ typedLength: number;
+ }
+ ): Promise<{ data: T; cleanup(): void }> {
+ assert(
+ srcByteOffset % type.BYTES_PER_ELEMENT === 0,
+ 'srcByteOffset must be a multiple of BYTES_PER_ELEMENT'
+ );
+
+ const byteLength = typedLength * type.BYTES_PER_ELEMENT;
+ let mappable: GPUBuffer;
+ let mapOffset: number | undefined, mapSize: number | undefined, subarrayByteStart: number;
+ if (method === 'copy') {
+ ({ mappable, subarrayByteStart } = this.createAlignedCopyForMapRead(
+ src,
+ byteLength,
+ srcByteOffset
+ ));
+ } else if (method === 'map') {
+ mappable = src;
+ mapOffset = roundDown(srcByteOffset, 8);
+ mapSize = align(byteLength, 4);
+ subarrayByteStart = srcByteOffset - mapOffset;
+ } else {
+ unreachable();
+ }
+
+ assert(subarrayByteStart % type.BYTES_PER_ELEMENT === 0);
+ const subarrayStart = subarrayByteStart / type.BYTES_PER_ELEMENT;
+
+ // 2. Map the staging buffer, and create the TypedArray from it.
+ await mappable.mapAsync(GPUMapMode.READ, mapOffset, mapSize);
+ const mapped = new type(mappable.getMappedRange(mapOffset, mapSize));
+ const data = mapped.subarray(subarrayStart, typedLength) as T;
+
+ return {
+ data,
+ cleanup() {
+ mappable.unmap();
+ mappable.destroy();
+ },
+ };
+ }
+
+ /**
+ * Expect a GPUBuffer's contents to pass the provided check.
+ *
+ * A library of checks can be found in {@link webgpu/util/check_contents}.
+ */
+ expectGPUBufferValuesPassCheck<T extends TypedArrayBufferView>(
+ src: GPUBuffer,
+ check: (actual: T) => Error | undefined,
+ {
+ srcByteOffset = 0,
+ type,
+ typedLength,
+ method = 'copy',
+ mode = 'fail',
+ }: {
+ srcByteOffset?: number;
+ type: TypedArrayBufferViewConstructor<T>;
+ typedLength: number;
+ method?: 'copy' | 'map';
+ mode?: 'fail' | 'warn';
+ }
+ ) {
+ const readbackPromise = this.readGPUBufferRangeTyped(src, {
+ srcByteOffset,
+ type,
+ typedLength,
+ method,
+ });
+ this.eventualAsyncExpectation(async niceStack => {
+ const readback = await readbackPromise;
+ this.expectOK(check(readback.data), { mode, niceStack });
+ readback.cleanup();
+ });
+ }
+
+ /**
+ * Expect a GPUBuffer's contents to equal the values in the provided TypedArray.
+ */
+ expectGPUBufferValuesEqual(
+ src: GPUBuffer,
+ expected: TypedArrayBufferView,
+ srcByteOffset: number = 0,
+ { method = 'copy', mode = 'fail' }: { method?: 'copy' | 'map'; mode?: 'fail' | 'warn' } = {}
+ ): void {
+ this.expectGPUBufferValuesPassCheck(src, a => checkElementsEqual(a, expected), {
+ srcByteOffset,
+ type: expected.constructor as TypedArrayBufferViewConstructor,
+ typedLength: expected.length,
+ method,
+ mode,
+ });
+ }
+
+ /**
+ * Expect a buffer to consist exclusively of rows of some repeated expected value. The size of
+ * `expectedValue` must be 1, 2, or any multiple of 4 bytes. Rows in the buffer are expected to be
+ * zero-padded out to `bytesPerRow`. `minBytesPerRow` is the number of bytes per row that contain
+ * actual (non-padding) data and must be an exact multiple of the byte-length of `expectedValue`.
+ */
+ expectGPUBufferRepeatsSingleValue(
+ buffer: GPUBuffer,
+ {
+ expectedValue,
+ numRows,
+ minBytesPerRow,
+ bytesPerRow,
+ }: {
+ expectedValue: ArrayBuffer;
+ numRows: number;
+ minBytesPerRow: number;
+ bytesPerRow: number;
+ }
+ ) {
+ const valueSize = expectedValue.byteLength;
+ assert(valueSize === 1 || valueSize === 2 || valueSize % 4 === 0);
+ assert(minBytesPerRow % valueSize === 0);
+ assert(bytesPerRow % 4 === 0);
+
+ // If the buffer is small enough, just generate the full expected buffer contents and check
+ // against them on the CPU.
+ const kMaxBufferSizeToCheckOnCpu = 256 * 1024;
+ const bufferSize = bytesPerRow * (numRows - 1) + minBytesPerRow;
+ if (bufferSize <= kMaxBufferSizeToCheckOnCpu) {
+ const valueBytes = Array.from(new Uint8Array(expectedValue));
+ const rowValues = new Array(minBytesPerRow / valueSize).fill(valueBytes);
+ const rowBytes = new Uint8Array([].concat(...rowValues));
+ const expectedContents = new Uint8Array(bufferSize);
+ range(numRows, row => expectedContents.set(rowBytes, row * bytesPerRow));
+ this.expectGPUBufferValuesEqual(buffer, expectedContents);
+ return;
+ }
+
+ // Copy into a buffer suitable for STORAGE usage.
+ const storageBuffer = this.device.createBuffer({
+ size: bufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(storageBuffer);
+
+ // This buffer conveys the data we expect to see for a single value read. Since we read 32 bits at
+ // a time, for values smaller than 32 bits we pad this expectation with repeated value data, or
+ // with zeroes if the width of a row in the buffer is less than 4 bytes. For value sizes larger
+ // than 32 bits, we assume they're a multiple of 32 bits and expect to read exact matches of
+ // `expectedValue` as-is.
+ const expectedDataSize = Math.max(4, valueSize);
+ const expectedDataBuffer = this.device.createBuffer({
+ size: expectedDataSize,
+ usage: GPUBufferUsage.STORAGE,
+ mappedAtCreation: true,
+ });
+ this.trackForCleanup(expectedDataBuffer);
+ const expectedData = new Uint32Array(expectedDataBuffer.getMappedRange());
+ if (valueSize === 1) {
+ const value = new Uint8Array(expectedValue)[0];
+ const values = new Array(Math.min(4, minBytesPerRow)).fill(value);
+ const padding = new Array(Math.max(0, 4 - values.length)).fill(0);
+ const expectedBytes = new Uint8Array(expectedData.buffer);
+ expectedBytes.set([...values, ...padding]);
+ } else if (valueSize === 2) {
+ const value = new Uint16Array(expectedValue)[0];
+ const expectedWords = new Uint16Array(expectedData.buffer);
+ expectedWords.set([value, minBytesPerRow > 2 ? value : 0]);
+ } else {
+ expectedData.set(new Uint32Array(expectedValue));
+ }
+ expectedDataBuffer.unmap();
+
+ // The output buffer has one 32-bit entry per buffer row. An entry's value will be 1 if every
+ // read from the corresponding row matches the expected data derived above, or 0 otherwise.
+ const resultBuffer = this.device.createBuffer({
+ size: numRows * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ this.trackForCleanup(resultBuffer);
+
+ const readsPerRow = Math.ceil(minBytesPerRow / expectedDataSize);
+ const reducer = `
+ struct Buffer { data: array<u32>, };
+ @group(0) @binding(0) var<storage, read> expected: Buffer;
+ @group(0) @binding(1) var<storage, read> in: Buffer;
+ @group(0) @binding(2) var<storage, read_write> out: Buffer;
+ @compute @workgroup_size(1) fn reduce(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ let rowBaseIndex = id.x * ${bytesPerRow / 4}u;
+ let readSize = ${expectedDataSize / 4}u;
+ out.data[id.x] = 1u;
+ for (var i: u32 = 0u; i < ${readsPerRow}u; i = i + 1u) {
+ let elementBaseIndex = rowBaseIndex + i * readSize;
+ for (var j: u32 = 0u; j < readSize; j = j + 1u) {
+ if (in.data[elementBaseIndex + j] != expected.data[j]) {
+ out.data[id.x] = 0u;
+ return;
+ }
+ }
+ }
+ }
+ `;
+
+ const pipeline = this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({ code: reducer }),
+ entryPoint: 'reduce',
+ },
+ });
+
+ const bindGroup = this.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: { buffer: expectedDataBuffer } },
+ { binding: 1, resource: { buffer: storageBuffer } },
+ { binding: 2, resource: { buffer: resultBuffer } },
+ ],
+ });
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyBufferToBuffer(buffer, 0, storageBuffer, 0, bufferSize);
+ const pass = commandEncoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(numRows);
+ pass.end();
+ this.device.queue.submit([commandEncoder.finish()]);
+
+ const expectedResults = new Array(numRows).fill(1);
+ this.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array(expectedResults));
+ }
+
+ // MAINTENANCE_TODO: add an expectContents for textures, which logs data: uris on failure
+
+ /**
+ * Expect a whole GPUTexture to have the single provided color.
+ */
+ expectSingleColor(
+ src: GPUTexture,
+ format: GPUTextureFormat,
+ {
+ size,
+ exp,
+ dimension = '2d',
+ slice = 0,
+ layout,
+ }: {
+ size: [number, number, number];
+ exp: PerTexelComponent<number>;
+ dimension?: GPUTextureDimension;
+ slice?: number;
+ layout?: TextureLayoutOptions;
+ }
+ ): void {
+ format = resolvePerAspectFormat(format, layout?.aspect);
+ const { byteLength, minBytesPerRow, bytesPerRow, rowsPerImage, mipSize } = getTextureCopyLayout(
+ format,
+ dimension,
+ size,
+ layout
+ );
+
+ const rep = kTexelRepresentationInfo[format as EncodableTextureFormat];
+ const expectedTexelData = rep.pack(rep.encode(exp));
+
+ const buffer = this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(buffer);
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyTextureToBuffer(
+ {
+ texture: src,
+ mipLevel: layout?.mipLevel,
+ origin: { x: 0, y: 0, z: slice },
+ aspect: layout?.aspect,
+ },
+ { buffer, bytesPerRow, rowsPerImage },
+ mipSize
+ );
+ this.queue.submit([commandEncoder.finish()]);
+
+ this.expectGPUBufferRepeatsSingleValue(buffer, {
+ expectedValue: expectedTexelData,
+ numRows: rowsPerImage,
+ minBytesPerRow,
+ bytesPerRow,
+ });
+ }
+
+ /** Return a GPUBuffer that data are going to be written into. */
+ private readSinglePixelFrom2DTexture(
+ src: GPUTexture,
+ format: SizedTextureFormat,
+ { x, y }: { x: number; y: number },
+ { slice = 0, layout }: { slice?: number; layout?: TextureLayoutOptions }
+ ): GPUBuffer {
+ const { byteLength, bytesPerRow, rowsPerImage } = getTextureSubCopyLayout(
+ format,
+ [1, 1],
+ layout
+ );
+ const buffer = this.device.createBuffer({
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
+ });
+ this.trackForCleanup(buffer);
+
+ const commandEncoder = this.device.createCommandEncoder();
+ commandEncoder.copyTextureToBuffer(
+ { texture: src, mipLevel: layout?.mipLevel, origin: { x, y, z: slice } },
+ { buffer, bytesPerRow, rowsPerImage },
+ [1, 1]
+ );
+ this.queue.submit([commandEncoder.finish()]);
+
+ return buffer;
+ }
+
+ /**
+ * Expect a single pixel of a 2D texture to have a particular byte representation.
+ *
+ * MAINTENANCE_TODO: Add check for values of depth/stencil, probably through sampling of shader
+ * MAINTENANCE_TODO: Can refactor this and expectSingleColor to use a similar base expect
+ */
+ expectSinglePixelIn2DTexture(
+ src: GPUTexture,
+ format: SizedTextureFormat,
+ { x, y }: { x: number; y: number },
+ {
+ exp,
+ slice = 0,
+ layout,
+ generateWarningOnly = false,
+ }: {
+ exp: Uint8Array;
+ slice?: number;
+ layout?: TextureLayoutOptions;
+ generateWarningOnly?: boolean;
+ }
+ ): void {
+ const buffer = this.readSinglePixelFrom2DTexture(src, format, { x, y }, { slice, layout });
+ this.expectGPUBufferValuesEqual(buffer, exp, 0, {
+ mode: generateWarningOnly ? 'warn' : 'fail',
+ });
+ }
+
+ /**
+ * Take a single pixel of a 2D texture, interpret it using a TypedArray of the `expected` type,
+ * and expect each value in that array to be between the corresponding "expected" values
+ * (either `a[i] <= actual[i] <= b[i]` or `a[i] >= actual[i] => b[i]`).
+ */
+ expectSinglePixelBetweenTwoValuesIn2DTexture(
+ src: GPUTexture,
+ format: SizedTextureFormat,
+ { x, y }: { x: number; y: number },
+ {
+ exp,
+ slice = 0,
+ layout,
+ generateWarningOnly = false,
+ checkElementsBetweenFn = (act, [a, b]) => checkElementsBetween(act, [i => a[i], i => b[i]]),
+ }: {
+ exp: [TypedArrayBufferView, TypedArrayBufferView];
+ slice?: number;
+ layout?: TextureLayoutOptions;
+ generateWarningOnly?: boolean;
+ checkElementsBetweenFn?: (
+ actual: TypedArrayBufferView,
+ expected: readonly [TypedArrayBufferView, TypedArrayBufferView]
+ ) => Error | undefined;
+ }
+ ): void {
+ assert(exp[0].constructor === exp[1].constructor);
+ const constructor = exp[0].constructor as TypedArrayBufferViewConstructor;
+ assert(exp[0].length === exp[1].length);
+ const typedLength = exp[0].length;
+
+ const buffer = this.readSinglePixelFrom2DTexture(src, format, { x, y }, { slice, layout });
+ this.expectGPUBufferValuesPassCheck(buffer, a => checkElementsBetweenFn(a, exp), {
+ type: constructor,
+ typedLength,
+ mode: generateWarningOnly ? 'warn' : 'fail',
+ });
+ }
+
+ /**
+ * Equivalent to {@link expectSinglePixelBetweenTwoValuesIn2DTexture} but uses a special check func
+ * to interpret incoming values as float16
+ */
+ expectSinglePixelBetweenTwoValuesFloat16In2DTexture(
+ src: GPUTexture,
+ format: SizedTextureFormat,
+ { x, y }: { x: number; y: number },
+ {
+ exp,
+ slice = 0,
+ layout,
+ generateWarningOnly = false,
+ }: {
+ exp: [Uint16Array, Uint16Array];
+ slice?: number;
+ layout?: TextureLayoutOptions;
+ generateWarningOnly?: boolean;
+ }
+ ): void {
+ this.expectSinglePixelBetweenTwoValuesIn2DTexture(
+ src,
+ format,
+ { x, y },
+ {
+ exp,
+ slice,
+ layout,
+ generateWarningOnly,
+ checkElementsBetweenFn: checkElementsFloat16Between,
+ }
+ );
+ }
+
+ /**
+ * Emulate a texture to buffer copy by using a compute shader
+ * to load texture value of a single pixel and write to a storage buffer.
+ * For sample count == 1, the buffer contains only one value of the sample.
+ * For sample count > 1, the buffer contains (N = sampleCount) values sorted
+ * in the order of their sample index [0, sampleCount - 1]
+ *
+ * This can be useful when the texture to buffer copy is not available to the texture format
+ * e.g. (depth24plus), or when the texture is multisampled.
+ *
+ * MAINTENANCE_TODO: extend to read multiple pixels with given origin and size.
+ *
+ * @returns storage buffer containing the copied value from the texture.
+ */
+ copySinglePixelTextureToBufferUsingComputePass(
+ type: ScalarType,
+ componentCount: number,
+ textureView: GPUTextureView,
+ sampleCount: number
+ ): GPUBuffer {
+ const textureSrcCode =
+ sampleCount === 1
+ ? `@group(0) @binding(0) var src: texture_2d<${type}>;`
+ : `@group(0) @binding(0) var src: texture_multisampled_2d<${type}>;`;
+ const code = `
+ struct Buffer {
+ data: array<${type}>,
+ };
+
+ ${textureSrcCode}
+ @group(0) @binding(1) var<storage, read_write> dst : Buffer;
+
+ @compute @workgroup_size(1) fn main() {
+ var coord = vec2<i32>(0, 0);
+ for (var sampleIndex = 0; sampleIndex < ${sampleCount};
+ sampleIndex = sampleIndex + 1) {
+ let o = sampleIndex * ${componentCount};
+ let v = textureLoad(src, coord, sampleIndex);
+ for (var component = 0; component < ${componentCount}; component = component + 1) {
+ dst.data[o + component] = v[component];
+ }
+ }
+ }
+ `;
+ const computePipeline = this.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: this.device.createShaderModule({
+ code,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const storageBuffer = this.device.createBuffer({
+ size: sampleCount * type.size * componentCount,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
+ });
+ this.trackForCleanup(storageBuffer);
+
+ const uniformBindGroup = this.device.createBindGroup({
+ layout: computePipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: textureView,
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: storageBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(computePipeline);
+ pass.setBindGroup(0, uniformBindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ return storageBuffer;
+ }
+
+ /**
+ * Expect the specified WebGPU error to be generated when running the provided function.
+ */
+ expectGPUError<R>(filter: GPUErrorFilter, fn: () => R, shouldError: boolean = true): R {
+ // If no error is expected, we let the scope surrounding the test catch it.
+ if (!shouldError) {
+ return fn();
+ }
+
+ this.device.pushErrorScope(filter);
+ const returnValue = fn();
+ const promise = this.device.popErrorScope();
+
+ this.eventualAsyncExpectation(async niceStack => {
+ const error = await promise;
+
+ let failed = false;
+ switch (filter) {
+ case 'out-of-memory':
+ failed = !(error instanceof GPUOutOfMemoryError);
+ break;
+ case 'validation':
+ failed = !(error instanceof GPUValidationError);
+ break;
+ }
+
+ if (failed) {
+ niceStack.message = `Expected ${filter} error`;
+ this.rec.expectationFailed(niceStack);
+ } else {
+ niceStack.message = `Captured ${filter} error`;
+ if (error instanceof GPUValidationError) {
+ niceStack.message += ` - ${error.message}`;
+ }
+ this.rec.debug(niceStack);
+ }
+ });
+
+ return returnValue;
+ }
+
+ /**
+ * Expect a validation error inside the callback.
+ *
+ * Tests should always do just one WebGPU call in the callback, to make sure that's what's tested.
+ */
+ expectValidationError(fn: () => void, shouldError: boolean = true): void {
+ // If no error is expected, we let the scope surrounding the test catch it.
+ if (shouldError) {
+ this.device.pushErrorScope('validation');
+ }
+
+ // Note: A return value is not allowed for the callback function. This is to avoid confusion
+ // about what the actual behavior would be; either of the following could be reasonable:
+ // - Make expectValidationError async, and have it await on fn(). This causes an async split
+ // between pushErrorScope and popErrorScope, so if the caller doesn't `await` on
+ // expectValidationError (either accidentally or because it doesn't care to do so), then
+ // other test code will be (nondeterministically) caught by the error scope.
+ // - Make expectValidationError NOT await fn(), but just execute its first block (until the
+ // first await) and return the return value (a Promise). This would be confusing because it
+ // would look like the error scope includes the whole async function, but doesn't.
+ // If we do decide we need to return a value, we should use the latter semantic.
+ const returnValue = fn() as unknown;
+ assert(
+ returnValue === undefined,
+ 'expectValidationError callback should not return a value (or be async)'
+ );
+
+ if (shouldError) {
+ const promise = this.device.popErrorScope();
+
+ this.eventualAsyncExpectation(async niceStack => {
+ const gpuValidationError = await promise;
+ if (!gpuValidationError) {
+ niceStack.message = 'Validation succeeded unexpectedly.';
+ this.rec.validationFailed(niceStack);
+ } else if (gpuValidationError instanceof GPUValidationError) {
+ niceStack.message = `Validation failed, as expected - ${gpuValidationError.message}`;
+ this.rec.debug(niceStack);
+ }
+ });
+ }
+ }
+
+ /**
+ * Expects that the device should be lost for a particular reason at the teardown of the test.
+ */
+ expectDeviceLost(reason: GPUDeviceLostReason): void {
+ assert(this.provider !== undefined, 'internal error: GPUDevice missing?');
+ this.provider.expectDeviceLost(reason);
+ }
+
+ /**
+ * Create a GPUBuffer with the specified contents and usage.
+ *
+ * MAINTENANCE_TODO: Several call sites would be simplified if this took ArrayBuffer as well.
+ */
+ makeBufferWithContents(dataArray: TypedArrayBufferView, usage: GPUBufferUsageFlags): GPUBuffer {
+ return this.trackForCleanup(makeBufferWithContents(this.device, dataArray, usage));
+ }
+
+ /**
+ * Creates a texture with the contents of a TexelView.
+ */
+ makeTextureWithContents(
+ texelView: TexelView,
+ desc: Omit<GPUTextureDescriptor, 'format'>
+ ): GPUTexture {
+ return this.trackForCleanup(makeTextureWithContents(this.device, texelView, desc));
+ }
+
+ /**
+ * Create a GPUTexture with multiple mip levels, each having the specified contents.
+ */
+ createTexture2DWithMipmaps(mipmapDataArray: TypedArrayBufferView[]): GPUTexture {
+ const format = 'rgba8unorm';
+ const mipLevelCount = mipmapDataArray.length;
+ const textureSizeMipmap0 = 1 << (mipLevelCount - 1);
+ const texture = this.device.createTexture({
+ mipLevelCount,
+ size: { width: textureSizeMipmap0, height: textureSizeMipmap0, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
+ });
+ this.trackForCleanup(texture);
+
+ const textureEncoder = this.device.createCommandEncoder();
+ for (let i = 0; i < mipLevelCount; i++) {
+ const { byteLength, bytesPerRow, rowsPerImage, mipSize } = getTextureCopyLayout(
+ format,
+ '2d',
+ [textureSizeMipmap0, textureSizeMipmap0, 1],
+ { mipLevel: i }
+ );
+
+ const data: Uint8Array = new Uint8Array(byteLength);
+ const mipLevelData = mipmapDataArray[i];
+ assert(rowsPerImage === mipSize[0]); // format is rgba8unorm and block size should be 1
+ for (let r = 0; r < rowsPerImage; r++) {
+ const o = r * bytesPerRow;
+ for (let c = o, end = o + mipSize[1] * 4; c < end; c += 4) {
+ data[c] = mipLevelData[0];
+ data[c + 1] = mipLevelData[1];
+ data[c + 2] = mipLevelData[2];
+ data[c + 3] = mipLevelData[3];
+ }
+ }
+ const buffer = this.makeBufferWithContents(
+ data,
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
+ );
+
+ textureEncoder.copyBufferToTexture(
+ { buffer, bytesPerRow, rowsPerImage },
+ { texture, mipLevel: i, origin: [0, 0, 0] },
+ mipSize
+ );
+ }
+ this.device.queue.submit([textureEncoder.finish()]);
+
+ return texture;
+ }
+
+ /**
+ * Returns a GPUCommandEncoder, GPUComputePassEncoder, GPURenderPassEncoder, or
+ * GPURenderBundleEncoder, and a `finish` method returning a GPUCommandBuffer.
+ * Allows testing methods which have the same signature across multiple encoder interfaces.
+ *
+ * @example
+ * ```
+ * g.test('popDebugGroup')
+ * .params(u => u.combine('encoderType', kEncoderTypes))
+ * .fn(t => {
+ * const { encoder, finish } = t.createEncoder(t.params.encoderType);
+ * encoder.popDebugGroup();
+ * });
+ *
+ * g.test('writeTimestamp')
+ * .params(u => u.combine('encoderType', ['non-pass', 'compute pass', 'render pass'] as const)
+ * .fn(t => {
+ * const { encoder, finish } = t.createEncoder(t.params.encoderType);
+ * // Encoder type is inferred, so `writeTimestamp` can be used even though it doesn't exist
+ * // on GPURenderBundleEncoder.
+ * encoder.writeTimestamp(args);
+ * });
+ * ```
+ */
+ createEncoder<T extends EncoderType>(
+ encoderType: T,
+ {
+ attachmentInfo,
+ occlusionQuerySet,
+ }: {
+ attachmentInfo?: GPURenderBundleEncoderDescriptor;
+ occlusionQuerySet?: GPUQuerySet;
+ } = {}
+ ): CommandBufferMaker<T> {
+ const fullAttachmentInfo = {
+ // Defaults if not overridden:
+ colorFormats: ['rgba8unorm'],
+ sampleCount: 1,
+ // Passed values take precedent.
+ ...attachmentInfo,
+ } as const;
+
+ switch (encoderType) {
+ case 'non-pass': {
+ const encoder = this.device.createCommandEncoder();
+
+ return new CommandBufferMaker(this, encoder, () => {
+ return encoder.finish();
+ });
+ }
+ case 'render bundle': {
+ const device = this.device;
+ const rbEncoder = device.createRenderBundleEncoder(fullAttachmentInfo);
+ const pass = this.createEncoder('render pass', { attachmentInfo });
+
+ return new CommandBufferMaker(this, rbEncoder, () => {
+ pass.encoder.executeBundles([rbEncoder.finish()]);
+ return pass.finish();
+ });
+ }
+ case 'compute pass': {
+ const commandEncoder = this.device.createCommandEncoder();
+ const encoder = commandEncoder.beginComputePass();
+
+ return new CommandBufferMaker(this, encoder, () => {
+ encoder.end();
+ return commandEncoder.finish();
+ });
+ }
+ case 'render pass': {
+ const makeAttachmentView = (format: GPUTextureFormat) =>
+ this.trackForCleanup(
+ this.device.createTexture({
+ size: [16, 16, 1],
+ format,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ sampleCount: fullAttachmentInfo.sampleCount,
+ })
+ ).createView();
+
+ let depthStencilAttachment: GPURenderPassDepthStencilAttachment | undefined = undefined;
+ if (fullAttachmentInfo.depthStencilFormat !== undefined) {
+ depthStencilAttachment = {
+ view: makeAttachmentView(fullAttachmentInfo.depthStencilFormat),
+ depthReadOnly: fullAttachmentInfo.depthReadOnly,
+ stencilReadOnly: fullAttachmentInfo.stencilReadOnly,
+ };
+ if (
+ kTextureFormatInfo[fullAttachmentInfo.depthStencilFormat].depth &&
+ !fullAttachmentInfo.depthReadOnly
+ ) {
+ depthStencilAttachment.depthClearValue = 0;
+ depthStencilAttachment.depthLoadOp = 'clear';
+ depthStencilAttachment.depthStoreOp = 'discard';
+ }
+ if (
+ kTextureFormatInfo[fullAttachmentInfo.depthStencilFormat].stencil &&
+ !fullAttachmentInfo.stencilReadOnly
+ ) {
+ depthStencilAttachment.stencilClearValue = 1;
+ depthStencilAttachment.stencilLoadOp = 'clear';
+ depthStencilAttachment.stencilStoreOp = 'discard';
+ }
+ }
+ const passDesc: GPURenderPassDescriptor = {
+ colorAttachments: Array.from(fullAttachmentInfo.colorFormats, format =>
+ format
+ ? {
+ view: makeAttachmentView(format),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ }
+ : null
+ ),
+ depthStencilAttachment,
+ occlusionQuerySet,
+ };
+
+ const commandEncoder = this.device.createCommandEncoder();
+ const encoder = commandEncoder.beginRenderPass(passDesc);
+ return new CommandBufferMaker(this, encoder, () => {
+ encoder.end();
+ return commandEncoder.finish();
+ });
+ }
+ }
+ unreachable();
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/README.txt
new file mode 100644
index 0000000000..aa7a983b04
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/README.txt
@@ -0,0 +1,7 @@
+Tests to check that the WebGPU IDL is correctly implemented, for examples that objects exposed
+exactly the correct members, and that methods throw when passed incomplete dictionaries.
+
+See https://github.com/gpuweb/cts/issues/332
+
+TODO: exposed.html.ts: Test all WebGPU interfaces instead of just some of them.
+TODO: Check prototype chains. (Add a helper in IDLTest for this.)
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constants/flags.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constants/flags.spec.ts
new file mode 100644
index 0000000000..ca78892fb4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/constants/flags.spec.ts
@@ -0,0 +1,79 @@
+export const description = `
+Test the values of flags interfaces (e.g. GPUTextureUsage).
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { IDLTest } from '../idl_test.js';
+
+export const g = makeTestGroup(IDLTest);
+
+const kBufferUsageExp = {
+ MAP_READ: 0x0001,
+ MAP_WRITE: 0x0002,
+ COPY_SRC: 0x0004,
+ COPY_DST: 0x0008,
+ INDEX: 0x0010,
+ VERTEX: 0x0020,
+ UNIFORM: 0x0040,
+ STORAGE: 0x0080,
+ INDIRECT: 0x0100,
+ QUERY_RESOLVE: 0x0200,
+};
+g.test('BufferUsage,count').fn(t => {
+ t.assertMemberCount(GPUBufferUsage, kBufferUsageExp);
+});
+g.test('BufferUsage,values')
+ .params(u => u.combine('key', Object.keys(kBufferUsageExp)))
+ .fn(t => {
+ const { key } = t.params;
+ t.assertMember(GPUBufferUsage, kBufferUsageExp, key);
+ });
+
+const kTextureUsageExp = {
+ COPY_SRC: 0x01,
+ COPY_DST: 0x02,
+ TEXTURE_BINDING: 0x04,
+ STORAGE_BINDING: 0x08,
+ RENDER_ATTACHMENT: 0x10,
+};
+g.test('TextureUsage,count').fn(t => {
+ t.assertMemberCount(GPUTextureUsage, kTextureUsageExp);
+});
+g.test('TextureUsage,values')
+ .params(u => u.combine('key', Object.keys(kTextureUsageExp)))
+ .fn(t => {
+ const { key } = t.params;
+ t.assertMember(GPUTextureUsage, kTextureUsageExp, key);
+ });
+
+const kColorWriteExp = {
+ RED: 0x1,
+ GREEN: 0x2,
+ BLUE: 0x4,
+ ALPHA: 0x8,
+ ALL: 0xf,
+};
+g.test('ColorWrite,count').fn(t => {
+ t.assertMemberCount(GPUColorWrite, kColorWriteExp);
+});
+g.test('ColorWrite,values')
+ .params(u => u.combine('key', Object.keys(kColorWriteExp)))
+ .fn(t => {
+ const { key } = t.params;
+ t.assertMember(GPUColorWrite, kColorWriteExp, key);
+ });
+
+const kShaderStageExp = {
+ VERTEX: 0x1,
+ FRAGMENT: 0x2,
+ COMPUTE: 0x4,
+};
+g.test('ShaderStage,count').fn(t => {
+ t.assertMemberCount(GPUShaderStage, kShaderStageExp);
+});
+g.test('ShaderStage,values')
+ .params(u => u.combine('key', Object.keys(kShaderStageExp)))
+ .fn(t => {
+ const { key } = t.params;
+ t.assertMember(GPUShaderStage, kShaderStageExp, key);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.html.ts
new file mode 100644
index 0000000000..7aee998a9f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.html.ts
@@ -0,0 +1,52 @@
+// WPT-specific test checking that WebGPU is available iff isSecureContext.
+
+import { assert } from '../../common/util/util.js';
+
+const items = [
+ globalThis.navigator.gpu,
+ globalThis.GPU,
+ globalThis.GPUAdapter,
+ globalThis.GPUAdapterInfo,
+ globalThis.GPUBindGroup,
+ globalThis.GPUBindGroupLayout,
+ globalThis.GPUBuffer,
+ globalThis.GPUBufferUsage,
+ globalThis.GPUCanvasContext,
+ globalThis.GPUColorWrite,
+ globalThis.GPUCommandBuffer,
+ globalThis.GPUCommandEncoder,
+ globalThis.GPUCompilationInfo,
+ globalThis.GPUCompilationMessage,
+ globalThis.GPUComputePassEncoder,
+ globalThis.GPUComputePipeline,
+ globalThis.GPUDevice,
+ globalThis.GPUDeviceLostInfo,
+ globalThis.GPUError,
+ globalThis.GPUExternalTexture,
+ globalThis.GPUMapMode,
+ globalThis.GPUOutOfMemoryError,
+ globalThis.GPUPipelineLayout,
+ globalThis.GPUQuerySet,
+ globalThis.GPUQueue,
+ globalThis.GPURenderBundle,
+ globalThis.GPURenderBundleEncoder,
+ globalThis.GPURenderPassEncoder,
+ globalThis.GPURenderPipeline,
+ globalThis.GPUSampler,
+ globalThis.GPUShaderModule,
+ globalThis.GPUShaderStage,
+ globalThis.GPUSupportedLimits,
+ globalThis.GPUTexture,
+ globalThis.GPUTextureUsage,
+ globalThis.GPUTextureView,
+ globalThis.GPUUncapturedErrorEvent,
+ globalThis.GPUValidationError,
+];
+
+for (const item of items) {
+ if (globalThis.isSecureContext) {
+ assert(item !== undefined, 'Item/interface should be exposed on secure context');
+ } else {
+ assert(item === undefined, 'Item/interface should not be exposed on insecure context');
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.http.html b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.http.html
new file mode 100644
index 0000000000..589460d499
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.http.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>WebGPU exposed items (non-HTTPS)</title>
+ <meta name=assert content="WebGPU should not be exposed on a non-[SecureContext]">
+ <link rel=help href='https://gpuweb.github.io/gpuweb/'>
+ <script type=module src=exposed.html.js></script>
+ </head>
+ <body></body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.https.html
new file mode 100644
index 0000000000..6b369f07ca
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/exposed.https.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <title>WebGPU exposed items (HTTPS)</title>
+ <meta charset=utf-8>
+ <meta name=assert content="All specified WebGPU items/interfaces should be exposed, on a [SecureContext]">
+ <link rel=help href='https://gpuweb.github.io/gpuweb/'>
+ <script type=module src=exposed.html.js></script>
+ </head>
+ <body></body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/idl/idl_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/idl_test.ts
new file mode 100644
index 0000000000..82111c6d4f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/idl/idl_test.ts
@@ -0,0 +1,40 @@
+import { Fixture } from '../../common/framework/fixture.js';
+import { getGPU } from '../../common/util/navigator_gpu.js';
+import { assert } from '../../common/util/util.js';
+
+interface UnknownObject {
+ [k: string]: unknown;
+}
+
+/**
+ * Base fixture for testing the exposed interface is correct (without actually using WebGPU).
+ */
+export class IDLTest extends Fixture {
+ async init(): Promise<void> {
+ // Ensure the GPU provider is initialized
+ getGPU();
+ }
+
+ /**
+ * Asserts that a member of an IDL interface has the expected value.
+ */
+ assertMember(act: UnknownObject, exp: UnknownObject, key: string) {
+ assert(key in act, () => `Expected key ${key} missing`);
+ assert(act[key] === exp[key], () => `Value of [${key}] was ${act[key]}, expected ${exp[key]}`);
+ }
+
+ /**
+ * Asserts that an IDL interface has the same number of keys as the
+ *
+ * MAINTENANCE_TODO: add a way to check for the types of keys with unknown values, like methods and attributes
+ * MAINTENANCE_TODO: handle extensions
+ */
+ assertMemberCount(act: UnknownObject, exp: UnknownObject) {
+ const expKeys = Object.keys(exp);
+ const actKeys = Object.keys(act);
+ assert(
+ actKeys.length === expKeys.length,
+ () => `Had ${actKeys.length} keys, expected ${expKeys.length}`
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/listing.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/listing.ts
new file mode 100644
index 0000000000..823639c692
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/listing.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/no-restricted-paths */
+import { TestSuiteListing } from '../common/internal/test_suite_listing.js';
+import { makeListing } from '../common/tools/crawl.js';
+
+export const listing: Promise<TestSuiteListing> = makeListing(__filename);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/README.txt
new file mode 100644
index 0000000000..3aadba27ee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/README.txt
@@ -0,0 +1 @@
+Tests for full coverage of the shaders that can be passed to WebGPU. \ No newline at end of file
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/README.txt
new file mode 100644
index 0000000000..52598f02ab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/README.txt
@@ -0,0 +1 @@
+Tests that check the result of valid shader execution.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/evaluation_order.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/evaluation_order.spec.ts
new file mode 100644
index 0000000000..0dfdd81165
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/evaluation_order.spec.ts
@@ -0,0 +1,484 @@
+export const description = `
+Execution Tests for evaluation order of expressions
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { GPUTest } from '../../gpu_test.js';
+import { TypeI32 } from '../../util/conversion.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const common_source = `
+var<private> a : i32 = 2;
+var<private> b : i32 = 3;
+var<private> c : i32 = 4;
+
+var<private> arr2D : array<array<i32, 10>, 10> = array<array<i32, 10>, 10>(
+ array<i32, 10>( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
+ array<i32, 10>(10, 11, 12, 13, 14, 15, 16, 17, 18, 19),
+ array<i32, 10>(20, 22, 22, 23, 24, 25, 26, 27, 28, 29),
+ array<i32, 10>(30, 32, 32, 33, 34, 35, 36, 37, 38, 33),
+ array<i32, 10>(40, 42, 42, 43, 44, 45, 46, 47, 48, 44),
+ array<i32, 10>(50, 52, 52, 53, 54, 55, 56, 57, 58, 55),
+ array<i32, 10>(60, 62, 62, 63, 64, 65, 66, 67, 68, 66),
+ array<i32, 10>(70, 72, 72, 73, 74, 75, 76, 77, 78, 77),
+ array<i32, 10>(80, 82, 82, 83, 84, 85, 86, 87, 88, 98),
+ array<i32, 10>(90, 92, 92, 93, 94, 95, 96, 97, 98, 99),
+);
+
+var<private> arr1D_zero : array<i32, 50>;
+var<private> arr2D_zero : array<array<i32, 50>, 50>;
+var<private> arr3D_zero : array<array<array<i32, 50>, 50>, 50>;
+
+var<private> vec4_zero : vec4<i32>;
+
+struct S {
+ x : i32,
+ y : i32,
+ z : i32,
+}
+
+fn mul(p1 : ptr<private, i32>, multiplicand : i32) -> i32 {
+ *p1 = *p1 * multiplicand;
+ return *p1;
+}
+
+fn add(p1 : ptr<private, i32>, addend : i32) -> i32 {
+ *p1 = *p1 + addend;
+ return *p1;
+}
+
+fn sub_mul3(a : i32, a_mul : i32, b : i32, b_mul : i32, c : i32, c_mul : i32) -> i32 {
+ return a * a_mul - b * b_mul - c * c_mul;
+}
+
+fn sub_mul4(a : i32, a_mul : i32, b : i32, b_mul : i32, c : i32, c_mul : i32, d : i32, d_mul : i32) -> i32 {
+ return a * a_mul - b * b_mul - c * c_mul - d * d_mul;
+}
+
+fn set_vec4_x(p : ptr<private, vec4<i32>>, v : i32) -> i32 {
+ (*p).x = v;
+ return 0;
+}
+
+fn make_S(init : i32) -> S {
+ return S(init, init, init);
+}
+
+fn mul3_ret0(p1 : ptr<private, i32>, p2 : ptr<private, i32>, p3 : ptr<private, i32>, multiplicand : i32) -> i32 {
+ *p1 = *p1 * multiplicand;
+ *p2 = *p2 * multiplicand;
+ *p3 = *p3 * multiplicand;
+ return 0;
+}
+`;
+
+g.test('binary_arith')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr')
+ .desc('Tests order of evaluation of arithmetic binary expressions.')
+ .paramsSimple([
+ {
+ name: 'BothSE', // SE = Side Effects
+ _body: 'return mul(&a, 10) - mul(&a, 10);',
+ _result: 20 - 200,
+ },
+ {
+ name: 'LeftSE', //
+ _body: 'return mul(&a, 10) - a;',
+ _result: 20 - 20,
+ },
+ {
+ name: 'RightSE', //
+ _body: 'return a - mul(&a, 10);',
+ _result: 2 - 20,
+ },
+ {
+ name: 'ThreeSE',
+ _body: 'return mul(&a, 10) - mul(&a, 10) - mul(&a, 10);',
+ _result: 20 - 200 - 2000,
+ },
+ {
+ name: 'LeftmostSE',
+ _body: 'return mul3_ret0(&a, &b, &c, 10) - a - b - c;',
+ _result: 0 - 20 - 30 - 40,
+ },
+ {
+ name: 'RightmostSE', //
+ _body: 'return a - b - c - mul3_ret0(&a, &b, &c, 10);',
+ _result: 2 - 3 - 4 - 0,
+ },
+ {
+ name: 'MiddleSE', //
+ _body: 'return a - b - mul3_ret0(&a, &b, &c, 10) - c;',
+ _result: 2 - 3 - 0 - 40,
+ },
+ {
+ name: 'LiteralAndSEAndVar', //
+ _body: 'return 1 - mul(&a, 10) - a;',
+ _result: 1 - 20 - 20,
+ },
+ {
+ name: 'VarAndSEAndLiteral', //
+ _body: 'return a - mul(&a, 10) - 1;',
+ _result: 2 - 20 - 1,
+ },
+ {
+ name: 'SEAndVarAndLiteral', //
+ _body: 'return mul(&a, 10) - a - 1;',
+ _result: 20 - 20 - 1,
+ },
+ {
+ name: 'VarAndLiteralAndSE', //
+ _body: 'return a - 1 - mul(&a, 10);',
+ _result: 2 - 1 - 20,
+ },
+ {
+ name: 'MemberAccessAndSE',
+ _body: 'return vec4_zero.x + set_vec4_x(&vec4_zero, 42);',
+ _result: 0,
+ },
+ {
+ name: 'SEAndMemberAccess',
+ _body: 'return set_vec4_x(&vec4_zero, 42) + vec4_zero.x;',
+ _result: 42,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('binary_logical')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#logical-expr')
+ .desc('Tests order of evaluation of logical binary expressions.')
+ .paramsSimple([
+ {
+ name: 'BothSE',
+ _body:
+ 'let r = (add(&a, 1) == 3) && (add(&a, 1) == 4);' + //
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'LeftSE',
+ _body:
+ 'let r = (add(&a, 1) == 3) && (a == 3);' + //
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'RightSE',
+ _body:
+ 'let r = (a == 2) && (add(&a, 1) == 3);' + //
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'LeftmostSE',
+ _body:
+ 'let r = (mul3_ret0(&a, &b, &c, 10) == 0) && (a == 20) && (b == 30) && (c == 40);' +
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'RightmostSE',
+ _body:
+ 'let r = (a == 2) && (b == 3) && (c == 4) && (mul3_ret0(&a, &b, &c, 10) == 0);' +
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'MiddleSE',
+ _body:
+ 'let r = (a == 2) && (b == 3) && (mul3_ret0(&a, &b, &c, 10) == 0) && (c == 40);' +
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'ShortCircuit_And_LhsOnly',
+ _body:
+ // rhs should not execute
+ 'let t = (a != 2) && (mul(&a, 10) == 20);' + //
+ 'return a;',
+ _result: 2,
+ },
+ {
+ name: 'ShortCircuit_And_LhsAndRhs',
+ _body:
+ // rhs should execute
+ 'let t = (a == 2) && (mul(&a, 10) == 20);' + //
+ 'return a;',
+ _result: 20,
+ },
+ {
+ name: 'ShortCircuit_Or_LhsOnly',
+ _body:
+ // rhs should not execute
+ 'let t = (a == 2) || (mul(&a, 10) == 20);' + //
+ 'let r = (a == 2);' +
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'ShortCircuit_Or_LhsAndRhs',
+ _body:
+ // rhs should execute
+ 'let t = (a != 2) || (mul(&a, 10) == 20);' + //
+ 'let r = (a == 20);' +
+ 'return i32(r);',
+ _result: 1,
+ },
+ {
+ name: 'NoShortCircuit_And',
+ _body:
+ // rhs should execute
+ 'let t = (a != 2) & (mul(&a, 10) == 20);' + //
+ 'return a;',
+ _result: 20,
+ },
+ {
+ name: 'NoShortCircuit_Or',
+ _body:
+ // rhs should execute
+ 'let t = (a == 2) | (mul(&a, 10) == 20);' + //
+ 'return a;',
+ _result: 20,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('binary_mixed')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#logical-expr')
+ .desc('Tests order of evaluation of both arithmetic and logical binary expressions.')
+ .paramsSimple([
+ {
+ name: 'ArithAndLogical',
+ _body: 'return mul(&a, 10) - i32(mul(&a, 10) == 200 && mul(&a, 10) == 2000);',
+ _result: 20 - 1,
+ },
+ {
+ name: 'LogicalAndArith',
+ _body: 'return i32(mul(&a, 10) == 20 && mul(&a, 10) == 200) - mul(&a, 10);',
+ _result: 1 - 2000,
+ },
+ {
+ name: 'ArithAndLogical_ShortCircuit',
+ _body: 'return mul(&a, 10) - i32(mul(&a, 10) != 200 && mul(&a, 10) == 2000);',
+ _result: 20 - 0,
+ },
+ {
+ name: 'LogicalAndArith_ShortCircuit',
+ _body: 'return i32(mul(&a, 10) != 20 && mul(&a, 10) == 200) - mul(&a, 10);',
+ _result: 0 - 200,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('call')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls')
+ .desc('Tests order of evaluation of call expressions.')
+ .paramsSimple([
+ {
+ name: 'OneSE', //
+ _body: 'return sub_mul3(mul(&a, 2), 2, a, 3, 3, 4);',
+ _result: 4 * 2 - 4 * 3 - 3 * 4,
+ },
+ {
+ name: 'AllSE',
+ _body: 'return sub_mul3(mul(&a, 2), 2, mul(&a, 2), 3, mul(&a, 2), 4);',
+ _result: 4 * 2 - 8 * 3 - 16 * 4,
+ },
+ {
+ name: 'MiddleNotSE',
+ _body: 'return sub_mul3(mul(&a, 2), 2, a, 3, mul(&a, 2), 4);',
+ _result: 4 * 2 - 4 * 3 - 8 * 4,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('index_accessor')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#array-access-expr')
+ .desc('Tests order of evaluation of index accessor expressions.')
+ .paramsSimple([
+ {
+ name: 'LeftSE', //
+ _body: 'return arr2D[mul(&a, 2)][a];',
+ _result: 4 * 10 + 4,
+ },
+ {
+ name: 'RightSE', //
+ _body: 'return arr2D[a][mul(&a, 2)];',
+ _result: 2 * 10 + 4,
+ },
+ {
+ name: 'BothSE',
+ _body: 'return arr2D[mul(&a, 2)][mul(&a, 2)];',
+ _result: 4 * 10 + 8,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('assignment')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#assignment')
+ .desc('Tests order of evaluation of assignment statements.')
+ .paramsSimple([
+ {
+ name: 'ToArray1D',
+ _body:
+ 'arr1D_zero[mul(&a, 2)] = mul(&a, 2);' + //
+ 'return arr1D_zero[8];',
+ _result: 4,
+ },
+ {
+ name: 'ToArray2D',
+ _body:
+ 'arr2D_zero[mul(&a, 2)][mul(&a, 2)] = mul(&a, 2);' + //
+ 'return arr2D_zero[8][16];',
+ _result: 4,
+ },
+ {
+ name: 'ToArrayFromArray',
+ _body:
+ 'arr2D_zero[4][8] = 123;' +
+ 'arr1D_zero[mul(&a, 2)] = arr2D_zero[mul(&a, 2)][mul(&a, 2)];' +
+ 'return arr1D_zero[16];',
+ _result: 123,
+ },
+ {
+ name: 'ToArrayIndexedByArrayIndexedBySE',
+ _body:
+ 'var arr1 = arr1D_zero;' +
+ 'var arr2 = arr1D_zero;' +
+ 'arr2[8] = 3;' +
+ 'arr1[arr2[mul(&a, 2)]] = mul(&a, 2);' +
+ 'return arr1[3];',
+ _result: 4,
+ },
+ {
+ name: 'ToVec_BothSE',
+ _body:
+ 'a = 0;' +
+ 'vec4_zero[add(&a, 1)] = add(&a, 1);' + //
+ 'return vec4_zero[2];',
+ _result: 1,
+ },
+ {
+ name: 'ToVec_LeftSE',
+ _body:
+ 'vec4_zero[add(&a, 1)] = a;' + //
+ 'return vec4_zero[3];',
+ _result: 2,
+ },
+ {
+ name: 'ToVec_RightSE',
+ _body:
+ 'vec4_zero[a] = add(&a, 1);' + //
+ 'return vec4_zero[3];',
+ _result: 3,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('type_constructor')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#type-constructor-expr')
+ .desc('Tests order of evaluation of type constructor expressions.')
+ .paramsSimple([
+ {
+ name: 'Struct',
+ _body:
+ 'let r = S(mul(&a, 2), mul(&a, 2), mul(&a, 2));' + //
+ 'return sub_mul3(r.x, 2, r.y, 3, r.z, 4);',
+ _result: 4 * 2 - 8 * 3 - 16 * 4,
+ },
+ {
+ name: 'Array1D',
+ _body:
+ 'let r = array<i32, 3>(mul(&a, 2), mul(&a, 2), mul(&a, 2));' + //
+ 'return sub_mul3(r[0], 2, r[1], 3, r[2], 4);',
+ _result: 4 * 2 - 8 * 3 - 16 * 4,
+ },
+ {
+ name: 'Array2D',
+ _body:
+ 'let r = array<array<i32, 2>, 2>(array<i32, 2>(mul(&a, 2), mul(&a, 2)), array<i32, 2>(mul(&a, 2), mul(&a, 2)));' +
+ 'return sub_mul4(r[0][0], 2, r[0][1], 3, r[1][0], 4, r[1][1], 5);',
+ _result: 4 * 2 - 8 * 3 - 16 * 4 - 32 * 5,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+g.test('member_accessor')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#struct-access-expr')
+ .specURL('https://gpuweb.github.io/gpuweb/wgsl/#vector-access-expr')
+ .desc('Tests order of evaluation of member accessor expressions.')
+ .paramsSimple([
+ {
+ name: 'Vec',
+ _body: 'return vec3(mul(&a, 2)).x - vec3(mul(&a, 3)).x;',
+ _result: 4 - 12,
+ },
+ {
+ name: 'Struct',
+ _body: 'return make_S(mul(&a, 2)).x - make_S(mul(&a, 3)).x;',
+ _result: 4 - 12,
+ },
+ ])
+ .fn(t => run(t, t.params._body, t.params._result));
+
+function run(t: GPUTest, body: string, result: number) {
+ // WGSL source
+ const source =
+ common_source +
+ `
+fn test_body() -> i32 {
+ ${body}
+}
+
+@group(0) @binding(0) var<storage, read_write> output : i32;
+
+@compute @workgroup_size(1)
+fn main() {
+ output = test_body();
+}
+`;
+
+ // Construct a buffer to hold the results of the expression tests
+ const outputBufferSize = 4; // result : i32
+ const outputBuffer = t.device.createBuffer({
+ size: outputBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+
+ const module = t.device.createShaderModule({ code: source });
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+
+ const group = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: outputBuffer } }],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, group);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+
+ t.queue.submit([encoder.finish()]);
+
+ const checkExpectation = (outputData: Uint8Array) => {
+ const output = TypeI32.read(outputData, 0);
+ const got = output.value;
+ const expected = result;
+ if (got !== expected) {
+ return new Error(`returned: ${got}, expected: ${expected}`);
+ }
+ return undefined;
+ };
+
+ t.expectGPUBufferValuesPassCheck(outputBuffer, checkExpectation, {
+ type: Uint8Array,
+ typedLength: outputBufferSize,
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts
new file mode 100644
index 0000000000..dcfc2b2a3f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/binary.ts
@@ -0,0 +1,9 @@
+import { ExpressionBuilder } from '../expression.js';
+
+/* @returns an ExpressionBuilder that evaluates a binary operation */
+export function binary(op: string): ExpressionBuilder {
+ return values => {
+ const values_str = values.map(v => `(${v})`);
+ return `(${values_str.join(op)})`;
+ };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts
new file mode 100644
index 0000000000..6a708564d9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bitwise.spec.ts
@@ -0,0 +1,220 @@
+export const description = `
+Execution Tests for the bitwise binary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { i32, scalarType, u32 } from '../../../../util/conversion.js';
+import { allInputSources, run } from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('bitwise_or')
+ .specURL('https://www.w3.org/TR/WGSL/#bit-expr')
+ .desc(
+ `
+e1 | e2: T
+T is i32, u32, vecN<i32>, or vecN<u32>
+
+Bitwise-or. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u
+ .combine('type', ['i32', 'u32'] as const)
+ .combine('inputSource', allInputSources)
+ .combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const type = scalarType(t.params.type);
+ const V = t.params.type === 'i32' ? i32 : u32;
+ const cases = [
+ // Static patterns
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b01010010001001010010001001010010), V(0b10100100010010100100010010100100)],
+ expected: V(0b11110110011011110110011011110110),
+ },
+ ];
+ // Permute all combinations of a single bit being set for the LHS and RHS
+ for (let i = 0; i < 32; i++) {
+ const lhs = 1 << i;
+ for (let j = 0; j < 32; j++) {
+ const rhs = 1 << j;
+ cases.push({
+ input: [V(lhs), V(rhs)],
+ expected: V(lhs | rhs),
+ });
+ }
+ }
+ await run(t, binary('|'), [type, type], type, t.params, cases);
+ });
+
+g.test('bitwise_and')
+ .specURL('https://www.w3.org/TR/WGSL/#bit-expr')
+ .desc(
+ `
+e1 & e2: T
+T is i32, u32, vecN<i32>, or vecN<u32>
+
+Bitwise-and. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u
+ .combine('type', ['i32', 'u32'] as const)
+ .combine('inputSource', allInputSources)
+ .combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const type = scalarType(t.params.type);
+ const V = t.params.type === 'i32' ? i32 : u32;
+ const cases = [
+ // Static patterns
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b10100100010010100100010010100100), V(0b11111111111111111111111111111111)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b10100100010010100100010010100100)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b01010010001001010010001001010010), V(0b01011011101101011011101101011011)],
+ expected: V(0b01010010001001010010001001010010),
+ },
+ ];
+ // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS
+ for (let i = 0; i < 32; i++) {
+ const lhs = 1 << i;
+ for (let j = 0; j < 32; j++) {
+ const rhs = 0xffffffff ^ (1 << j);
+ cases.push({
+ input: [V(lhs), V(rhs)],
+ expected: V(lhs & rhs),
+ });
+ }
+ }
+ await run(t, binary('&'), [type, type], type, t.params, cases);
+ });
+
+g.test('bitwise_exclusive_or')
+ .specURL('https://www.w3.org/TR/WGSL/#bit-expr')
+ .desc(
+ `
+e1 ^ e2: T
+T is i32, u32, vecN<i32>, or vecN<u32>
+
+Bitwise-exclusive-or. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u
+ .combine('type', ['i32', 'u32'] as const)
+ .combine('inputSource', allInputSources)
+ .combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const type = scalarType(t.params.type);
+ const V = t.params.type === 'i32' ? i32 : u32;
+ const cases = [
+ // Static patterns
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b00000000000000000000000000000000)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b00000000000000000000000000000000)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b11111111111111111111111111111111)],
+ expected: V(0b11111111111111111111111111111111),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b11111111111111111111111111111111)],
+ expected: V(0b00000000000000000000000000000000),
+ },
+ {
+ input: [V(0b10100100010010100100010010100100), V(0b00000000000000000000000000000000)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b10100100010010100100010010100100), V(0b11111111111111111111111111111111)],
+ expected: V(0b01011011101101011011101101011011),
+ },
+ {
+ input: [V(0b00000000000000000000000000000000), V(0b10100100010010100100010010100100)],
+ expected: V(0b10100100010010100100010010100100),
+ },
+ {
+ input: [V(0b11111111111111111111111111111111), V(0b10100100010010100100010010100100)],
+ expected: V(0b01011011101101011011101101011011),
+ },
+ {
+ input: [V(0b01010010001001010010001001010010), V(0b01011011101101011011101101011011)],
+ expected: V(0b00001001100100001001100100001001),
+ },
+ ];
+ // Permute all combinations of a single bit being set for the LHS and all but one bit set for the RHS
+ for (let i = 0; i < 32; i++) {
+ const lhs = 1 << i;
+ for (let j = 0; j < 32; j++) {
+ const rhs = 0xffffffff ^ (1 << j);
+ cases.push({
+ input: [V(lhs), V(rhs)],
+ expected: V(lhs ^ rhs),
+ });
+ }
+ }
+ await run(t, binary('^'), [type, type], type, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts
new file mode 100644
index 0000000000..d3c426920a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/bool_logical.spec.ts
@@ -0,0 +1,143 @@
+export const description = `
+Execution Tests for the boolean binary logical expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { bool, TypeBool } from '../../../../util/conversion.js';
+import { allInputSources, run } from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// Short circuiting vs no short circuiting is not tested here, it is covered in
+// src/webgpu/shader/execution/evaluation_order.spec.ts
+
+g.test('and')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 & e2
+Logical "and". Component-wise when T is a vector. Evaluates both e1 and e2.
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(false) },
+ { input: [bool(true), bool(false)], expected: bool(false) },
+ { input: [bool(false), bool(true)], expected: bool(false) },
+ { input: [bool(true), bool(true)], expected: bool(true) },
+ ];
+
+ await run(t, binary('&'), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
+
+g.test('and_short_circuit')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 && e2
+short_circuiting "and". Yields true if both e1 and e2 are true; evaluates e2 only if e1 is true.
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(false) },
+ { input: [bool(true), bool(false)], expected: bool(false) },
+ { input: [bool(false), bool(true)], expected: bool(false) },
+ { input: [bool(true), bool(true)], expected: bool(true) },
+ ];
+
+ await run(t, binary('&&'), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
+
+g.test('or')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 | e2
+Logical "or". Component-wise when T is a vector. Evaluates both e1 and e2.
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(false) },
+ { input: [bool(true), bool(false)], expected: bool(true) },
+ { input: [bool(false), bool(true)], expected: bool(true) },
+ { input: [bool(true), bool(true)], expected: bool(true) },
+ ];
+
+ await run(t, binary('|'), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
+
+g.test('or_short_circuit')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 || e2
+short_circuiting "and". Yields true if both e1 and e2 are true; evaluates e2 only if e1 is true.
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(false) },
+ { input: [bool(true), bool(false)], expected: bool(true) },
+ { input: [bool(false), bool(true)], expected: bool(true) },
+ { input: [bool(true), bool(true)], expected: bool(true) },
+ ];
+
+ await run(t, binary('||'), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
+
+g.test('equals')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 == e2
+Equality. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(true) },
+ { input: [bool(true), bool(false)], expected: bool(false) },
+ { input: [bool(false), bool(true)], expected: bool(false) },
+ { input: [bool(true), bool(true)], expected: bool(true) },
+ ];
+
+ await run(t, binary('=='), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
+
+g.test('not_equals')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: e1 != e2
+Equality. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = [
+ { input: [bool(false), bool(false)], expected: bool(false) },
+ { input: [bool(true), bool(false)], expected: bool(true) },
+ { input: [bool(false), bool(true)], expected: bool(true) },
+ { input: [bool(true), bool(true)], expected: bool(false) },
+ ];
+
+ await run(t, binary('!='), [TypeBool, TypeBool], TypeBool, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_arithmetic.spec.ts
new file mode 100644
index 0000000000..84d32191bc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_arithmetic.spec.ts
@@ -0,0 +1,194 @@
+export const description = `
+Execution Tests for the f32 arithmetic binary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { TypeF32 } from '../../../../util/conversion.js';
+import {
+ additionInterval,
+ divisionInterval,
+ multiplicationInterval,
+ remainderInterval,
+ subtractionInterval,
+} from '../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import { allInputSources, generateBinaryToF32IntervalCases, run } from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('binary/f32_arithmetic', {
+ addition_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ additionInterval
+ );
+ },
+ addition_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ additionInterval
+ );
+ },
+ subtraction_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ subtractionInterval
+ );
+ },
+ subtraction_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ subtractionInterval
+ );
+ },
+ multiplication_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ multiplicationInterval
+ );
+ },
+ multiplication_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ multiplicationInterval
+ );
+ },
+ division_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ divisionInterval
+ );
+ },
+ division_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ divisionInterval
+ );
+ },
+ remainder_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ remainderInterval
+ );
+ },
+ remainder_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ remainderInterval
+ );
+ },
+});
+
+g.test('addition')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x + y
+Accuracy: Correctly rounded
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'addition_const' : 'addition_non_const'
+ );
+ await run(t, binary('+'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('subtraction')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x - y
+Accuracy: Correctly rounded
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'subtraction_const' : 'subtraction_non_const'
+ );
+ await run(t, binary('-'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('multiplication')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x * y
+Accuracy: Correctly rounded
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'multiplication_const' : 'multiplication_non_const'
+ );
+ await run(t, binary('*'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('division')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x / y
+Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'division_const' : 'division_non_const'
+ );
+ await run(t, binary('/'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('remainder')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x % y
+Accuracy: Derived from x - y * trunc(x/y)
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const'
+ );
+ await run(t, binary('%'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts
new file mode 100644
index 0000000000..21f2810d01
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts
@@ -0,0 +1,260 @@
+export const description = `
+Execution Tests for the f32 logical binary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { anyOf } from '../../../../util/compare.js';
+import { bool, f32, Scalar, TypeBool, TypeF32 } from '../../../../util/conversion.js';
+import { flushSubnormalScalarF32, vectorF32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import { allInputSources, Case, run } from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+/**
+ * @returns a test case for the provided left hand & right hand values and truth function.
+ * Handles quantization and subnormals.
+ */
+function makeCase(
+ lhs: number,
+ rhs: number,
+ truthFunc: (lhs: Scalar, rhs: Scalar) => boolean
+): Case {
+ const f32_lhs = f32(lhs);
+ const f32_rhs = f32(rhs);
+ const lhs_options = new Set([f32_lhs, flushSubnormalScalarF32(f32_lhs)]);
+ const rhs_options = new Set([f32_rhs, flushSubnormalScalarF32(f32_rhs)]);
+ const expected: Array<Scalar> = [];
+ lhs_options.forEach(l => {
+ rhs_options.forEach(r => {
+ const result = bool(truthFunc(l, r));
+ if (!expected.includes(result)) {
+ expected.push(result);
+ }
+ });
+ });
+
+ return { input: [f32_lhs, f32_rhs], expected: anyOf(...expected) };
+}
+
+export const d = makeCaseCache('binary/f32_logical', {
+ equals_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) === (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ equals_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) === (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ not_equals_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) !== (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ not_equals_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) !== (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ less_than_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) < (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ less_than_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) < (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ less_equals_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) <= (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ less_equals_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) <= (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ greater_than_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) > (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ greater_than_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) > (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ greater_equals_non_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) >= (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+ greater_equals_const: () => {
+ const truthFunc = (lhs: Scalar, rhs: Scalar): boolean => {
+ return (lhs.value as number) >= (rhs.value as number);
+ };
+
+ return vectorF32Range(2).map(v => {
+ return makeCase(v[0], v[1], truthFunc);
+ });
+ },
+});
+
+g.test('equals')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x == y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'equals_const' : 'equals_non_const'
+ );
+ await run(t, binary('=='), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
+
+g.test('not_equals')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x != y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'not_equals_const' : 'not_equals_non_const'
+ );
+ await run(t, binary('!='), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
+
+g.test('less_than')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x < y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'less_than_const' : 'less_than_non_const'
+ );
+ await run(t, binary('<'), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
+
+g.test('less_equals')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x <= y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'less_equals_const' : 'less_equals_non_const'
+ );
+ await run(t, binary('<='), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
+
+g.test('greater_than')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x > y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'greater_than_const' : 'greater_than_non_const'
+ );
+ await run(t, binary('>'), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
+
+g.test('greater_equals')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x >= y
+Accuracy: Correct result
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'greater_equals_const' : 'greater_equals_non_const'
+ );
+ await run(t, binary('>='), [TypeF32, TypeF32], TypeBool, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts
new file mode 100644
index 0000000000..bbdb260a2d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/i32_arithmetic.spec.ts
@@ -0,0 +1,156 @@
+export const description = `
+Execution Tests for the i32 arithmetic binary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { kValue } from '../../../../util/constants.js';
+import { TypeI32 } from '../../../../util/conversion.js';
+import { fullI32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import { allInputSources, generateBinaryToI32Cases, run } from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('binary/i32_arithmetic', {
+ addition: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ return x + y;
+ });
+ },
+ subtraction: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ return x - y;
+ });
+ },
+ multiplication: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ division_non_const: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ if (y === 0) {
+ return x;
+ }
+ if (x === kValue.i32.negative.min && y === -1) {
+ return x;
+ }
+ return x / y;
+ });
+ },
+ division_const: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ if (y === 0) {
+ return undefined;
+ }
+ if (x === kValue.i32.negative.min && y === -1) {
+ return undefined;
+ }
+ return x / y;
+ });
+ },
+ remainder_non_const: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ if (y === 0) {
+ return 0;
+ }
+ if (x === kValue.i32.negative.min && y === -1) {
+ return 0;
+ }
+ return x % y;
+ });
+ },
+ remainder_const: () => {
+ return generateBinaryToI32Cases(fullI32Range(), fullI32Range(), (x, y) => {
+ if (y === 0) {
+ return undefined;
+ }
+ if (x === kValue.i32.negative.min && y === -1) {
+ return undefined;
+ }
+ return x % y;
+ });
+ },
+});
+
+g.test('addition')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x + y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('addition');
+ await run(t, binary('+'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('subtraction')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x - y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('subtraction');
+ await run(t, binary('-'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('multiplication')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x * y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('multiplication');
+ await run(t, binary('*'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('division')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x / y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'division_const' : 'division_non_const'
+ );
+ await run(t, binary('/'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('remainder')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: x % y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const'
+ );
+ await run(t, binary('%'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts
new file mode 100644
index 0000000000..bf7789a635
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/binary/u32_arithmetic.spec.ts
@@ -0,0 +1,213 @@
+export const description = `
+Execution Tests for the u32 arithmetic binary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { TypeU32, TypeVec } from '../../../../util/conversion.js';
+import { fullU32Range, sparseU32Range, vectorU32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import {
+ allInputSources,
+ generateBinaryToU32Cases,
+ generateU32VectorBinaryToVectorCases,
+ generateVectorU32BinaryToVectorCases,
+ run,
+} from '../expression.js';
+
+import { binary } from './binary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('binary/u32_arithmetic', {
+ addition: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ return x + y;
+ });
+ },
+ subtraction: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ return x - y;
+ });
+ },
+ multiplication: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ division_non_const: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ if (y === 0) {
+ return x;
+ }
+ return x / y;
+ });
+ },
+ division_const: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ if (y === 0) {
+ return undefined;
+ }
+ return x / y;
+ });
+ },
+ remainder_non_const: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ if (y === 0) {
+ return 0;
+ }
+ return x % y;
+ });
+ },
+ remainder_const: () => {
+ return generateBinaryToU32Cases(fullU32Range(), fullU32Range(), (x, y) => {
+ if (y === 0) {
+ return undefined;
+ }
+ return x % y;
+ });
+ },
+ multiplication_scalar_vector2: () => {
+ return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(2), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ multiplication_scalar_vector3: () => {
+ return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(3), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ multiplication_scalar_vector4: () => {
+ return generateU32VectorBinaryToVectorCases(sparseU32Range(), vectorU32Range(4), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ multiplication_vector2_scalar: () => {
+ return generateVectorU32BinaryToVectorCases(vectorU32Range(2), sparseU32Range(), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ multiplication_vector3_scalar: () => {
+ return generateVectorU32BinaryToVectorCases(vectorU32Range(3), sparseU32Range(), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+ multiplication_vector4_scalar: () => {
+ return generateVectorU32BinaryToVectorCases(vectorU32Range(4), sparseU32Range(), (x, y) => {
+ return Math.imul(x, y);
+ });
+ },
+});
+
+g.test('addition')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x + y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('addition');
+ await run(t, binary('+'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('subtraction')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x - y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('subtraction');
+ await run(t, binary('-'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('multiplication')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x * y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('multiplication');
+ await run(t, binary('*'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('division')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x / y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'division_const' : 'division_non_const'
+ );
+ await run(t, binary('/'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('remainder')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x % y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'remainder_const' : 'remainder_non_const'
+ );
+ await run(t, binary('%'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('multiplication_scalar_vector')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x * y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize_rhs', [2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const vec_size = t.params.vectorize_rhs;
+ const vec_type = TypeVec(vec_size, TypeU32);
+ const cases = await d.get(`multiplication_scalar_vector${vec_size}`);
+ await run(t, binary('*'), [TypeU32, vec_type], vec_type, t.params, cases);
+ });
+
+g.test('multiplication_vector_scalar')
+ .specURL('https://www.w3.org/TR/WGSL/#arithmetic-expr')
+ .desc(
+ `
+Expression: x * y
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize_lhs', [2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const vec_size = t.params.vectorize_lhs;
+ const vec_type = TypeVec(vec_size, TypeU32);
+ const cases = await d.get(`multiplication_vector${vec_size}_scalar`);
+ await run(t, binary('*'), [vec_type, TypeU32], vec_type, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts
new file mode 100644
index 0000000000..272d0190a5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/abs.spec.ts
@@ -0,0 +1,167 @@
+export const description = `
+Execution tests for the 'abs' builtin function
+
+S is AbstractInt, i32, or u32
+T is S or vecN<S>
+@const fn abs(e: T ) -> T
+The absolute value of e. Component-wise when T is a vector. If e is a signed
+integral scalar type and evaluates to the largest negative value, then the
+result is e. If e is an unsigned integral type, then the result is e.
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn abs(e: T ) -> T
+Returns the absolute value of e (e.g. e with a positive sign bit).
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kBit } from '../../../../../util/constants.js';
+import { i32Bits, TypeF32, TypeI32, TypeU32, u32Bits } from '../../../../../util/conversion.js';
+import { absInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('abs', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', absInterval);
+ },
+});
+
+g.test('abstract_int')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`abstract int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`unsigned int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ await run(t, builtin('abs'), [TypeU32], TypeU32, t.params, [
+ // Min and Max u32
+ { input: u32Bits(kBit.u32.min), expected: u32Bits(kBit.u32.min) },
+ { input: u32Bits(kBit.u32.max), expected: u32Bits(kBit.u32.max) },
+ // Powers of 2: -2^i: 0 =< i =< 31
+ { input: u32Bits(kBit.powTwo.to0), expected: u32Bits(kBit.powTwo.to0) },
+ { input: u32Bits(kBit.powTwo.to1), expected: u32Bits(kBit.powTwo.to1) },
+ { input: u32Bits(kBit.powTwo.to2), expected: u32Bits(kBit.powTwo.to2) },
+ { input: u32Bits(kBit.powTwo.to3), expected: u32Bits(kBit.powTwo.to3) },
+ { input: u32Bits(kBit.powTwo.to4), expected: u32Bits(kBit.powTwo.to4) },
+ { input: u32Bits(kBit.powTwo.to5), expected: u32Bits(kBit.powTwo.to5) },
+ { input: u32Bits(kBit.powTwo.to6), expected: u32Bits(kBit.powTwo.to6) },
+ { input: u32Bits(kBit.powTwo.to7), expected: u32Bits(kBit.powTwo.to7) },
+ { input: u32Bits(kBit.powTwo.to8), expected: u32Bits(kBit.powTwo.to8) },
+ { input: u32Bits(kBit.powTwo.to9), expected: u32Bits(kBit.powTwo.to9) },
+ { input: u32Bits(kBit.powTwo.to10), expected: u32Bits(kBit.powTwo.to10) },
+ { input: u32Bits(kBit.powTwo.to11), expected: u32Bits(kBit.powTwo.to11) },
+ { input: u32Bits(kBit.powTwo.to12), expected: u32Bits(kBit.powTwo.to12) },
+ { input: u32Bits(kBit.powTwo.to13), expected: u32Bits(kBit.powTwo.to13) },
+ { input: u32Bits(kBit.powTwo.to14), expected: u32Bits(kBit.powTwo.to14) },
+ { input: u32Bits(kBit.powTwo.to15), expected: u32Bits(kBit.powTwo.to15) },
+ { input: u32Bits(kBit.powTwo.to16), expected: u32Bits(kBit.powTwo.to16) },
+ { input: u32Bits(kBit.powTwo.to17), expected: u32Bits(kBit.powTwo.to17) },
+ { input: u32Bits(kBit.powTwo.to18), expected: u32Bits(kBit.powTwo.to18) },
+ { input: u32Bits(kBit.powTwo.to19), expected: u32Bits(kBit.powTwo.to19) },
+ { input: u32Bits(kBit.powTwo.to20), expected: u32Bits(kBit.powTwo.to20) },
+ { input: u32Bits(kBit.powTwo.to21), expected: u32Bits(kBit.powTwo.to21) },
+ { input: u32Bits(kBit.powTwo.to22), expected: u32Bits(kBit.powTwo.to22) },
+ { input: u32Bits(kBit.powTwo.to23), expected: u32Bits(kBit.powTwo.to23) },
+ { input: u32Bits(kBit.powTwo.to24), expected: u32Bits(kBit.powTwo.to24) },
+ { input: u32Bits(kBit.powTwo.to25), expected: u32Bits(kBit.powTwo.to25) },
+ { input: u32Bits(kBit.powTwo.to26), expected: u32Bits(kBit.powTwo.to26) },
+ { input: u32Bits(kBit.powTwo.to27), expected: u32Bits(kBit.powTwo.to27) },
+ { input: u32Bits(kBit.powTwo.to28), expected: u32Bits(kBit.powTwo.to28) },
+ { input: u32Bits(kBit.powTwo.to29), expected: u32Bits(kBit.powTwo.to29) },
+ { input: u32Bits(kBit.powTwo.to30), expected: u32Bits(kBit.powTwo.to30) },
+ { input: u32Bits(kBit.powTwo.to31), expected: u32Bits(kBit.powTwo.to31) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`signed int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ await run(t, builtin('abs'), [TypeI32], TypeI32, t.params, [
+ // Min and max i32
+ // If e evaluates to the largest negative value, then the result is e.
+ { input: i32Bits(kBit.i32.negative.min), expected: i32Bits(kBit.i32.negative.min) },
+ { input: i32Bits(kBit.i32.negative.max), expected: i32Bits(kBit.i32.positive.min) },
+ { input: i32Bits(kBit.i32.positive.max), expected: i32Bits(kBit.i32.positive.max) },
+ { input: i32Bits(kBit.i32.positive.min), expected: i32Bits(kBit.i32.positive.min) },
+ // input: -1 * pow(2, n), n = {-31, ..., 0 }, expected: pow(2, n), n = {-31, ..., 0}]
+ { input: i32Bits(kBit.negPowTwo.to0), expected: i32Bits(kBit.powTwo.to0) },
+ { input: i32Bits(kBit.negPowTwo.to1), expected: i32Bits(kBit.powTwo.to1) },
+ { input: i32Bits(kBit.negPowTwo.to2), expected: i32Bits(kBit.powTwo.to2) },
+ { input: i32Bits(kBit.negPowTwo.to3), expected: i32Bits(kBit.powTwo.to3) },
+ { input: i32Bits(kBit.negPowTwo.to4), expected: i32Bits(kBit.powTwo.to4) },
+ { input: i32Bits(kBit.negPowTwo.to5), expected: i32Bits(kBit.powTwo.to5) },
+ { input: i32Bits(kBit.negPowTwo.to6), expected: i32Bits(kBit.powTwo.to6) },
+ { input: i32Bits(kBit.negPowTwo.to7), expected: i32Bits(kBit.powTwo.to7) },
+ { input: i32Bits(kBit.negPowTwo.to8), expected: i32Bits(kBit.powTwo.to8) },
+ { input: i32Bits(kBit.negPowTwo.to9), expected: i32Bits(kBit.powTwo.to9) },
+ { input: i32Bits(kBit.negPowTwo.to10), expected: i32Bits(kBit.powTwo.to10) },
+ { input: i32Bits(kBit.negPowTwo.to11), expected: i32Bits(kBit.powTwo.to11) },
+ { input: i32Bits(kBit.negPowTwo.to12), expected: i32Bits(kBit.powTwo.to12) },
+ { input: i32Bits(kBit.negPowTwo.to13), expected: i32Bits(kBit.powTwo.to13) },
+ { input: i32Bits(kBit.negPowTwo.to14), expected: i32Bits(kBit.powTwo.to14) },
+ { input: i32Bits(kBit.negPowTwo.to15), expected: i32Bits(kBit.powTwo.to15) },
+ { input: i32Bits(kBit.negPowTwo.to16), expected: i32Bits(kBit.powTwo.to16) },
+ { input: i32Bits(kBit.negPowTwo.to17), expected: i32Bits(kBit.powTwo.to17) },
+ { input: i32Bits(kBit.negPowTwo.to18), expected: i32Bits(kBit.powTwo.to18) },
+ { input: i32Bits(kBit.negPowTwo.to19), expected: i32Bits(kBit.powTwo.to19) },
+ { input: i32Bits(kBit.negPowTwo.to20), expected: i32Bits(kBit.powTwo.to20) },
+ { input: i32Bits(kBit.negPowTwo.to21), expected: i32Bits(kBit.powTwo.to21) },
+ { input: i32Bits(kBit.negPowTwo.to22), expected: i32Bits(kBit.powTwo.to22) },
+ { input: i32Bits(kBit.negPowTwo.to23), expected: i32Bits(kBit.powTwo.to23) },
+ { input: i32Bits(kBit.negPowTwo.to24), expected: i32Bits(kBit.powTwo.to24) },
+ { input: i32Bits(kBit.negPowTwo.to25), expected: i32Bits(kBit.powTwo.to25) },
+ { input: i32Bits(kBit.negPowTwo.to26), expected: i32Bits(kBit.powTwo.to26) },
+ { input: i32Bits(kBit.negPowTwo.to27), expected: i32Bits(kBit.powTwo.to27) },
+ { input: i32Bits(kBit.negPowTwo.to28), expected: i32Bits(kBit.powTwo.to28) },
+ { input: i32Bits(kBit.negPowTwo.to29), expected: i32Bits(kBit.powTwo.to29) },
+ { input: i32Bits(kBit.negPowTwo.to30), expected: i32Bits(kBit.powTwo.to30) },
+ { input: i32Bits(kBit.negPowTwo.to31), expected: i32Bits(kBit.powTwo.to31) },
+ ]);
+ });
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`float 32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('abs'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts
new file mode 100644
index 0000000000..312b3160f2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acos.spec.ts
@@ -0,0 +1,61 @@
+export const description = `
+Execution tests for the 'acos' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn acos(e: T ) -> T
+Returns the arc cosine of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { acosInterval } from '../../../../../util/f32_interval.js';
+import { linearRange, fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const inputs = [
+ ...linearRange(-1, 1, 100), // acos is defined on [-1, 1]
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('acos', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', acosInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', acosInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('acos'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts
new file mode 100644
index 0000000000..89887d05ef
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/acosh.spec.ts
@@ -0,0 +1,65 @@
+export const description = `
+Execution tests for the 'acosh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn acosh(e: T ) -> T
+Returns the hyperbolic arc cosine of e. The result is 0 when e < 1.
+Computes the non-negative functional inverse of cosh.
+Component-wise when T is a vector.
+Note: The result is not mathematically meaningful when e < 1.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { acoshIntervals } from '../../../../../util/f32_interval.js';
+import { biasedRange, fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const inputs = [
+ ...biasedRange(1, 2, 100), // x near 1 can be problematic to implement
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('acosh', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', ...acoshIntervals);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', ...acoshIntervals);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('acosh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts
new file mode 100644
index 0000000000..9a2938c1d5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/all.spec.ts
@@ -0,0 +1,92 @@
+export const description = `
+Execution tests for the 'all' builtin function
+
+S is a bool
+T is S or vecN<S>
+@const fn all(e: T) -> bool
+Returns e if e is scalar.
+Returns true if each component of e is true if e is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ False,
+ True,
+ TypeBool,
+ TypeVec,
+ vec2,
+ vec3,
+ vec4,
+} from '../../../../../util/conversion.js';
+import { allInputSources, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('bool')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-builtin-functions')
+ .desc(`bool tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('overload', ['scalar', 'vec2', 'vec3', 'vec4'] as const)
+ )
+ .fn(async t => {
+ const overloads = {
+ scalar: {
+ type: TypeBool,
+ cases: [
+ { input: False, expected: False },
+ { input: True, expected: True },
+ ],
+ },
+ vec2: {
+ type: TypeVec(2, TypeBool),
+ cases: [
+ { input: vec2(False, False), expected: False },
+ { input: vec2(True, False), expected: False },
+ { input: vec2(False, True), expected: False },
+ { input: vec2(True, True), expected: True },
+ ],
+ },
+ vec3: {
+ type: TypeVec(3, TypeBool),
+ cases: [
+ { input: vec3(False, False, False), expected: False },
+ { input: vec3(True, False, False), expected: False },
+ { input: vec3(False, True, False), expected: False },
+ { input: vec3(True, True, False), expected: False },
+ { input: vec3(False, False, True), expected: False },
+ { input: vec3(True, False, True), expected: False },
+ { input: vec3(False, True, True), expected: False },
+ { input: vec3(True, True, True), expected: True },
+ ],
+ },
+ vec4: {
+ type: TypeVec(4, TypeBool),
+ cases: [
+ { input: vec4(False, False, False, False), expected: False },
+ { input: vec4(False, True, False, False), expected: False },
+ { input: vec4(False, False, True, False), expected: False },
+ { input: vec4(False, True, True, False), expected: False },
+ { input: vec4(False, False, False, True), expected: False },
+ { input: vec4(False, True, False, True), expected: False },
+ { input: vec4(False, False, True, True), expected: False },
+ { input: vec4(False, True, True, True), expected: False },
+ { input: vec4(True, False, False, False), expected: False },
+ { input: vec4(True, False, False, True), expected: False },
+ { input: vec4(True, False, True, False), expected: False },
+ { input: vec4(True, False, True, True), expected: False },
+ { input: vec4(True, True, False, False), expected: False },
+ { input: vec4(True, True, False, True), expected: False },
+ { input: vec4(True, True, True, False), expected: False },
+ { input: vec4(True, True, True, True), expected: True },
+ ],
+ },
+ };
+ const overload = overloads[t.params.overload];
+
+ await run(t, builtin('all'), [overload.type], TypeBool, t.params, overload.cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts
new file mode 100644
index 0000000000..19ed7d186f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/any.spec.ts
@@ -0,0 +1,92 @@
+export const description = `
+Execution tests for the 'any' builtin function
+
+S is a bool
+T is S or vecN<S>
+@const fn all(e) -> bool
+Returns e if e is scalar.
+Returns true if any component of e is true if e is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ False,
+ True,
+ TypeBool,
+ TypeVec,
+ vec2,
+ vec3,
+ vec4,
+} from '../../../../../util/conversion.js';
+import { allInputSources, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('bool')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-builtin-functions')
+ .desc(`bool tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('overload', ['scalar', 'vec2', 'vec3', 'vec4'] as const)
+ )
+ .fn(async t => {
+ const overloads = {
+ scalar: {
+ type: TypeBool,
+ cases: [
+ { input: False, expected: False },
+ { input: True, expected: True },
+ ],
+ },
+ vec2: {
+ type: TypeVec(2, TypeBool),
+ cases: [
+ { input: vec2(False, False), expected: False },
+ { input: vec2(True, False), expected: True },
+ { input: vec2(False, True), expected: True },
+ { input: vec2(True, True), expected: True },
+ ],
+ },
+ vec3: {
+ type: TypeVec(3, TypeBool),
+ cases: [
+ { input: vec3(False, False, False), expected: False },
+ { input: vec3(True, False, False), expected: True },
+ { input: vec3(False, True, False), expected: True },
+ { input: vec3(True, True, False), expected: True },
+ { input: vec3(False, False, True), expected: True },
+ { input: vec3(True, False, True), expected: True },
+ { input: vec3(False, True, True), expected: True },
+ { input: vec3(True, True, True), expected: True },
+ ],
+ },
+ vec4: {
+ type: TypeVec(4, TypeBool),
+ cases: [
+ { input: vec4(False, False, False, False), expected: False },
+ { input: vec4(False, True, False, False), expected: True },
+ { input: vec4(False, False, True, False), expected: True },
+ { input: vec4(False, True, True, False), expected: True },
+ { input: vec4(False, False, False, True), expected: True },
+ { input: vec4(False, True, False, True), expected: True },
+ { input: vec4(False, False, True, True), expected: True },
+ { input: vec4(False, True, True, True), expected: True },
+ { input: vec4(True, False, False, False), expected: True },
+ { input: vec4(True, False, False, True), expected: True },
+ { input: vec4(True, False, True, False), expected: True },
+ { input: vec4(True, False, True, True), expected: True },
+ { input: vec4(True, True, False, False), expected: True },
+ { input: vec4(True, True, False, True), expected: True },
+ { input: vec4(True, True, True, False), expected: True },
+ { input: vec4(True, True, True, True), expected: True },
+ ],
+ },
+ };
+ const overload = overloads[t.params.overload];
+
+ await run(t, builtin('any'), [overload.type], TypeBool, t.params, overload.cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/arrayLength.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/arrayLength.spec.ts
new file mode 100644
index 0000000000..0b1c62c773
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/arrayLength.spec.ts
@@ -0,0 +1,16 @@
+export const description = `
+Execution tests for the 'arrayLength' builtin function
+
+fn arrayLength(e: ptr<storage,array<T>> ) -> u32
+Returns the number of elements in the runtime-sized array.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('array')
+ .specURL('https://www.w3.org/TR/WGSL/#array-builtin-functions')
+ .desc(`array length tests`)
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts
new file mode 100644
index 0000000000..6ff62d2431
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asin.spec.ts
@@ -0,0 +1,61 @@
+export const description = `
+Execution tests for the 'asin' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn asin(e: T ) -> T
+Returns the arc sine of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { asinInterval } from '../../../../../util/f32_interval.js';
+import { linearRange, fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const inputs = [
+ ...linearRange(-1, 1, 100), // asin is defined on [-1, 1]
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('asin', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', asinInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', asinInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('asin'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts
new file mode 100644
index 0000000000..3c2b4fcc13
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/asinh.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'sinh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn asinh(e: T ) -> T
+Returns the hyperbolic arc sine of e.
+Computes the functional inverse of sinh.
+Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { asinhInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('asinh', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', asinhInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float test`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('asinh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts
new file mode 100644
index 0000000000..218ccfc792
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan.spec.ts
@@ -0,0 +1,76 @@
+export const description = `
+Execution tests for the 'atan' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn atan(e: T ) -> T
+Returns the arc tangent of e. Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { atanInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const inputs = [
+ // Known values
+ -Math.sqrt(3),
+ -1,
+ -1 / Math.sqrt(3),
+ 0,
+ 1,
+ 1 / Math.sqrt(3),
+ Math.sqrt(3),
+
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('atan', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', atanInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', atanInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('atan'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts
new file mode 100644
index 0000000000..c1ba3f4c51
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atan2.spec.ts
@@ -0,0 +1,71 @@
+export const description = `
+Execution tests for the 'atan2' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn atan2(e1: T ,e2: T ) -> T
+Returns the arc tangent of e1 over e2. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { atan2Interval } from '../../../../../util/f32_interval.js';
+import { linearRange, sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateBinaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('atan2', {
+ f32: () => {
+ // Using sparse, since there a N^2 cases being generated, but including extra values around 0, since that is where
+ // there is a discontinuity that implementations tend to behave badly at.
+ const numeric_range = [
+ ...sparseF32Range(),
+ ...linearRange(kValue.f32.negative.max, kValue.f32.positive.min, 10),
+ ];
+ return generateBinaryToF32IntervalCases(
+ numeric_range,
+ numeric_range,
+ 'unfiltered',
+ atan2Interval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('atan2'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts
new file mode 100644
index 0000000000..11267930c8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atanh.spec.ts
@@ -0,0 +1,68 @@
+export const description = `
+Execution tests for the 'atanh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn atanh(e: T ) -> T
+Returns the hyperbolic arc tangent of e. The result is 0 when abs(e) ≥ 1.
+Computes the functional inverse of tanh.
+Component-wise when T is a vector.
+Note: The result is not mathematically meaningful when abs(e) >= 1.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { atanhInterval } from '../../../../../util/f32_interval.js';
+import { biasedRange, fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const inputs = [
+ ...biasedRange(kValue.f32.negative.less_than_one, -0.9, 20), // discontinuity at x = -1
+ -1,
+ ...biasedRange(kValue.f32.positive.less_than_one, 0.9, 20), // discontinuity at x = 1
+ 1,
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('atanh', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', atanhInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', atanhInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('atanh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAdd.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAdd.spec.ts
new file mode 100644
index 0000000000..9dcae3a062
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAdd.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, add and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by adding with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('add')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicAdd(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAnd.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAnd.spec.ts
new file mode 100644
index 0000000000..3e896af780
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicAnd.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, and and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by anding with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('and')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicAnd(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicCompareExchangeWeak.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicCompareExchangeWeak.spec.ts
new file mode 100644
index 0000000000..9172e3a638
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicCompareExchangeWeak.spec.ts
@@ -0,0 +1,49 @@
+export const description = `
+Performs the following steps atomically:
+ * Load the original value pointed to by atomic_ptr.
+ * Compare the original value to the value v using an equality operation.
+ * Store the value v only if the result of the equality comparison was true.
+
+Returns a two member structure, where the first member, old_value, is the original
+value of the atomic object and the second member, exchanged, is whether or not
+the comparison succeeded.
+
+Note: the equality comparison may spuriously fail on some implementations.
+That is, the second component of the result vector may be false even if the first
+component of the result vector equals cmp.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('exchange')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicCompareExchangeWeak(atomic_ptr: ptr<SC, atomic<T>, read_write>, cmp: T, v: T) -> __atomic_compare_exchange_result<T>
+
+struct __atomic_compare_exchange_result<T> {
+ old_value : T, // old value stored in the atomic
+ exchanged : bool, // true if the exchange was done
+}
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicExchange.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicExchange.spec.ts
new file mode 100644
index 0000000000..cd7fa072c5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicExchange.spec.ts
@@ -0,0 +1,33 @@
+export const description = `
+Atomically stores the value v in the atomic object pointed to atomic_ptr and returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('exchange')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicExchange(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicLoad.spec.ts
new file mode 100644
index 0000000000..c2e6a7e3d8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicLoad.spec.ts
@@ -0,0 +1,34 @@
+export const description = `
+Returns the atomically loaded the value pointed to by atomic_ptr. It does not modify the object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-load')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('load')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-load')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicLoad(atomic_ptr: ptr<SC, atomic<T>, read_write>) -> T
+
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMax.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMax.spec.ts
new file mode 100644
index 0000000000..b321524839
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMax.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, max and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by taking the max with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('max')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicMax(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMin.spec.ts
new file mode 100644
index 0000000000..deea6a4105
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicMin.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, min and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by take the min with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('min')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicMin(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicOr.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicOr.spec.ts
new file mode 100644
index 0000000000..0b8f6cdc41
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicOr.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, or and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by or'ing with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('or')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicOr(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicStore.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicStore.spec.ts
new file mode 100644
index 0000000000..18b713ba2e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicStore.spec.ts
@@ -0,0 +1,33 @@
+export const description = `
+Atomically stores the value v in the atomic object pointed to by atomic_ptr.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-store')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('store')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-store')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicStore(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T)
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicSub.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicSub.spec.ts
new file mode 100644
index 0000000000..cd150c59fb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicSub.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, subtract and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by subtracting with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('sub')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicSub(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicXor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicXor.spec.ts
new file mode 100644
index 0000000000..d8efd32f11
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/atomicXor.spec.ts
@@ -0,0 +1,39 @@
+export const description = `
+Atomically read, xor and store value.
+
+ * Load the original value pointed to by atomic_ptr.
+ * Obtains a new value by xor'ing with the value v.
+ * Store the new value using atomic_ptr.
+
+Returns the original value stored in the atomic object.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+Atomic built-in functions must not be used in a vertex shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('xor')
+ .specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
+ .desc(
+ `
+SC is storage or workgroup
+T is i32 or u32
+
+fn atomicXor(atomic_ptr: ptr<SC, atomic<T>, read_write>, v: T) -> T
+`
+ )
+ .params(u =>
+ u.combine('SC', ['storage', 'uniform'] as const).combine('T', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts
new file mode 100644
index 0000000000..c54245b699
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/builtin.ts
@@ -0,0 +1,6 @@
+import { ExpressionBuilder } from '../../expression.js';
+
+/* @returns an ExpressionBuilder that calls the builtin with the given name */
+export function builtin(name: string): ExpressionBuilder {
+ return values => `${name}(${values.join(', ')})`;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts
new file mode 100644
index 0000000000..49626628ab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ceil.spec.ts
@@ -0,0 +1,72 @@
+export const description = `
+Execution tests for the 'ceil' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn ceil(e: T ) -> T
+Returns the ceiling of e. Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { ceilInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('ceil', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Small positive numbers
+ 0.1,
+ 0.9,
+ 1.0,
+ 1.1,
+ 1.9,
+ // Small negative numbers
+ -0.1,
+ -0.9,
+ -1.0,
+ -1.1,
+ -1.9,
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ ceilInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('ceil'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts
new file mode 100644
index 0000000000..9411d667be
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/clamp.spec.ts
@@ -0,0 +1,172 @@
+export const description = `
+Execution tests for the 'clamp' builtin function
+
+S is AbstractInt, i32, or u32
+T is S or vecN<S>
+@const fn clamp(e: T , low: T, high: T) -> T
+Returns min(max(e,low),high). Component-wise when T is a vector.
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const clamp(e: T , low: T , high: T) -> T
+Returns either min(max(e,low),high), or the median of the three values e, low, high.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kBit } from '../../../../../util/constants.js';
+import {
+ i32,
+ i32Bits,
+ Scalar,
+ TypeF32,
+ TypeI32,
+ TypeU32,
+ u32,
+ u32Bits,
+} from '../../../../../util/conversion.js';
+import { clampIntervals } from '../../../../../util/f32_interval.js';
+import { sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, generateTernaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('clamp', {
+ u32: () => {
+ // This array must be strictly increasing, since that ordering determines
+ // the expected values.
+ const test_values: Array<Scalar> = [
+ u32Bits(kBit.u32.min),
+ u32(1),
+ u32(2),
+ u32(0x70000000),
+ u32(0x80000000),
+ u32Bits(kBit.u32.max),
+ ];
+
+ return generateIntegerTestCases(test_values);
+ },
+ i32: () => {
+ // This array must be strictly increasing, since that ordering determines
+ // the expected values.
+ const test_values: Array<Scalar> = [
+ i32Bits(kBit.i32.negative.min),
+ i32(-2),
+ i32(-1),
+ i32(0),
+ i32(1),
+ i32(2),
+ i32Bits(0x70000000),
+ i32Bits(kBit.i32.positive.max),
+ ];
+
+ return generateIntegerTestCases(test_values);
+ },
+ f32_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'f32-only',
+ ...clampIntervals
+ );
+ },
+ f32_non_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'unfiltered',
+ ...clampIntervals
+ );
+ },
+});
+
+/**
+ * Calculates clamp using the min-max formula.
+ * clamp(e, f, g) = min(max(e, f), g)
+ *
+ * Operates on indices of an ascending sorted array, instead of the actual
+ * values to avoid rounding issues.
+ *
+ * @returns the index of the clamped value
+ */
+function calculateMinMaxClamp(ei: number, fi: number, gi: number): number {
+ return Math.min(Math.max(ei, fi), gi);
+}
+
+/** @returns a set of clamp test cases from an ascending list of integer values */
+function generateIntegerTestCases(test_values: Array<Scalar>): Array<Case> {
+ const cases = new Array<Case>();
+ test_values.forEach((e, ei) => {
+ test_values.forEach((f, fi) => {
+ test_values.forEach((g, gi) => {
+ const expected_idx = calculateMinMaxClamp(ei, fi, gi);
+ const expected = test_values[expected_idx];
+ cases.push({ input: [e, f, g], expected });
+ });
+ });
+ });
+ return cases;
+}
+
+g.test('abstract_int')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`abstract int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('u32');
+ await run(t, builtin('clamp'), [TypeU32, TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('i32');
+ await run(t, builtin('clamp'), [TypeI32, TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('clamp'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts
new file mode 100644
index 0000000000..c81b985dc5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cos.spec.ts
@@ -0,0 +1,68 @@
+export const description = `
+Execution tests for the 'cos' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn cos(e: T ) -> T
+Returns the cosine of e. Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { cosInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('cos', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Well defined accuracy range
+ ...linearRange(-Math.PI, Math.PI, 1000),
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ cosInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('cos'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts
new file mode 100644
index 0000000000..2d2e0fdf80
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cosh.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'cosh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn cosh(e: T ) -> T
+Returns the hyperbolic cosine of e. Component-wise when T is a vector
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { coshInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('cosh', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'f32-only', coshInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', coshInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('cosh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts
new file mode 100644
index 0000000000..cfae4bb6e0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countLeadingZeros.spec.ts
@@ -0,0 +1,250 @@
+export const description = `
+Execution tests for the 'countLeadingZeros' builtin function
+
+S is i32 or u32
+T is S or vecN<S>
+@const fn countLeadingZeros(e: T ) -> T
+The number of consecutive 0 bits starting from the most significant bit of e,
+when T is a scalar type.
+Component-wise when T is a vector.
+Also known as "clz" in some languages.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeU32, u32Bits, u32, TypeI32, i32Bits, i32 } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countLeadingZeros'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32(32) },
+
+ // One
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32(31) },
+
+ // 0's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32(30) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32(29) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32(28) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32(27) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32(26) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32(25) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32(24) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32(23) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32(22) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32(21) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32(20) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32(19) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32(18) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32(17) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32(16) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32(15) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32(14) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32(13) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32(12) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32(11) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32(10) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32(9) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32(8) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32(7) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32(6) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32(5) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32(4) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32(3) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32(2) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32(0) },
+
+ // 1's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000011), expected: u32(30) },
+ { input: u32Bits(0b00000000000000000000000000000111), expected: u32(29) },
+ { input: u32Bits(0b00000000000000000000000000001111), expected: u32(28) },
+ { input: u32Bits(0b00000000000000000000000000011111), expected: u32(27) },
+ { input: u32Bits(0b00000000000000000000000000111111), expected: u32(26) },
+ { input: u32Bits(0b00000000000000000000000001111111), expected: u32(25) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(24) },
+ { input: u32Bits(0b00000000000000000000000111111111), expected: u32(23) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(22) },
+ { input: u32Bits(0b00000000000000000000011111111111), expected: u32(21) },
+ { input: u32Bits(0b00000000000000000000111111111111), expected: u32(20) },
+ { input: u32Bits(0b00000000000000000001111111111111), expected: u32(19) },
+ { input: u32Bits(0b00000000000000000011111111111111), expected: u32(18) },
+ { input: u32Bits(0b00000000000000000111111111111111), expected: u32(17) },
+ { input: u32Bits(0b00000000000000001111111111111111), expected: u32(16) },
+ { input: u32Bits(0b00000000000000011111111111111111), expected: u32(15) },
+ { input: u32Bits(0b00000000000000111111111111111111), expected: u32(14) },
+ { input: u32Bits(0b00000000000001111111111111111111), expected: u32(13) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(12) },
+ { input: u32Bits(0b00000000000111111111111111111111), expected: u32(11) },
+ { input: u32Bits(0b00000000001111111111111111111111), expected: u32(10) },
+ { input: u32Bits(0b00000000011111111111111111111111), expected: u32(9) },
+ { input: u32Bits(0b00000000111111111111111111111111), expected: u32(8) },
+ { input: u32Bits(0b00000001111111111111111111111111), expected: u32(7) },
+ { input: u32Bits(0b00000011111111111111111111111111), expected: u32(6) },
+ { input: u32Bits(0b00000111111111111111111111111111), expected: u32(5) },
+ { input: u32Bits(0b00001111111111111111111111111111), expected: u32(4) },
+ { input: u32Bits(0b00011111111111111111111111111111), expected: u32(3) },
+ { input: u32Bits(0b00111111111111111111111111111111), expected: u32(2) },
+ { input: u32Bits(0b01111111111111111111111111111111), expected: u32(1) },
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32(0) },
+
+ // random after leading 1
+ { input: u32Bits(0b00000000000000000000000000000110), expected: u32(29) },
+ { input: u32Bits(0b00000000000000000000000000001101), expected: u32(28) },
+ { input: u32Bits(0b00000000000000000000000000011101), expected: u32(27) },
+ { input: u32Bits(0b00000000000000000000000000111001), expected: u32(26) },
+ { input: u32Bits(0b00000000000000000000000001101111), expected: u32(25) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(24) },
+ { input: u32Bits(0b00000000000000000000000111101111), expected: u32(23) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(22) },
+ { input: u32Bits(0b00000000000000000000011111110001), expected: u32(21) },
+ { input: u32Bits(0b00000000000000000000111011011101), expected: u32(20) },
+ { input: u32Bits(0b00000000000000000001101101111111), expected: u32(19) },
+ { input: u32Bits(0b00000000000000000011111111011111), expected: u32(18) },
+ { input: u32Bits(0b00000000000000000101111001110101), expected: u32(17) },
+ { input: u32Bits(0b00000000000000001101111011110111), expected: u32(16) },
+ { input: u32Bits(0b00000000000000011111111111110011), expected: u32(15) },
+ { input: u32Bits(0b00000000000000111111111110111111), expected: u32(14) },
+ { input: u32Bits(0b00000000000001111111011111111111), expected: u32(13) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(12) },
+ { input: u32Bits(0b00000000000111110101011110111111), expected: u32(11) },
+ { input: u32Bits(0b00000000001111101111111111110111), expected: u32(10) },
+ { input: u32Bits(0b00000000011111111111010000101111), expected: u32(9) },
+ { input: u32Bits(0b00000000111111111111001111111011), expected: u32(8) },
+ { input: u32Bits(0b00000001111111011111101111111111), expected: u32(7) },
+ { input: u32Bits(0b00000011101011111011110111111011), expected: u32(6) },
+ { input: u32Bits(0b00000111111110111111111111111111), expected: u32(5) },
+ { input: u32Bits(0b00001111000000011011011110111111), expected: u32(4) },
+ { input: u32Bits(0b00011110101111011111111111111111), expected: u32(3) },
+ { input: u32Bits(0b00110110111111100111111110111101), expected: u32(2) },
+ { input: u32Bits(0b01010111111101111111011111011111), expected: u32(1) },
+ { input: u32Bits(0b11100010011110101101101110101111), expected: u32(0) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countLeadingZeros'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32(32) },
+
+ // One
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32(31) },
+
+ // 0's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32(30) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32(29) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32(28) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32(27) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32(26) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32(25) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32(24) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32(23) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32(22) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32(21) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32(20) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32(19) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32(18) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32(17) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32(16) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32(15) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32(14) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32(13) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32(12) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32(11) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32(10) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32(9) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32(8) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32(7) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32(6) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32(5) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32(4) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32(3) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32(2) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32(0) },
+
+ // 1's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000011), expected: i32(30) },
+ { input: i32Bits(0b00000000000000000000000000000111), expected: i32(29) },
+ { input: i32Bits(0b00000000000000000000000000001111), expected: i32(28) },
+ { input: i32Bits(0b00000000000000000000000000011111), expected: i32(27) },
+ { input: i32Bits(0b00000000000000000000000000111111), expected: i32(26) },
+ { input: i32Bits(0b00000000000000000000000001111111), expected: i32(25) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(24) },
+ { input: i32Bits(0b00000000000000000000000111111111), expected: i32(23) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(22) },
+ { input: i32Bits(0b00000000000000000000011111111111), expected: i32(21) },
+ { input: i32Bits(0b00000000000000000000111111111111), expected: i32(20) },
+ { input: i32Bits(0b00000000000000000001111111111111), expected: i32(19) },
+ { input: i32Bits(0b00000000000000000011111111111111), expected: i32(18) },
+ { input: i32Bits(0b00000000000000000111111111111111), expected: i32(17) },
+ { input: i32Bits(0b00000000000000001111111111111111), expected: i32(16) },
+ { input: i32Bits(0b00000000000000011111111111111111), expected: i32(15) },
+ { input: i32Bits(0b00000000000000111111111111111111), expected: i32(14) },
+ { input: i32Bits(0b00000000000001111111111111111111), expected: i32(13) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(12) },
+ { input: i32Bits(0b00000000000111111111111111111111), expected: i32(11) },
+ { input: i32Bits(0b00000000001111111111111111111111), expected: i32(10) },
+ { input: i32Bits(0b00000000011111111111111111111111), expected: i32(9) },
+ { input: i32Bits(0b00000000111111111111111111111111), expected: i32(8) },
+ { input: i32Bits(0b00000001111111111111111111111111), expected: i32(7) },
+ { input: i32Bits(0b00000011111111111111111111111111), expected: i32(6) },
+ { input: i32Bits(0b00000111111111111111111111111111), expected: i32(5) },
+ { input: i32Bits(0b00001111111111111111111111111111), expected: i32(4) },
+ { input: i32Bits(0b00011111111111111111111111111111), expected: i32(3) },
+ { input: i32Bits(0b00111111111111111111111111111111), expected: i32(2) },
+ { input: i32Bits(0b01111111111111111111111111111111), expected: i32(1) },
+ { input: i32Bits(0b11111111111111111111111111111111), expected: i32(0) },
+
+ // random after leading 1
+ { input: i32Bits(0b00000000000000000000000000000110), expected: i32(29) },
+ { input: i32Bits(0b00000000000000000000000000001101), expected: i32(28) },
+ { input: i32Bits(0b00000000000000000000000000011101), expected: i32(27) },
+ { input: i32Bits(0b00000000000000000000000000111001), expected: i32(26) },
+ { input: i32Bits(0b00000000000000000000000001101111), expected: i32(25) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(24) },
+ { input: i32Bits(0b00000000000000000000000111101111), expected: i32(23) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(22) },
+ { input: i32Bits(0b00000000000000000000011111110001), expected: i32(21) },
+ { input: i32Bits(0b00000000000000000000111011011101), expected: i32(20) },
+ { input: i32Bits(0b00000000000000000001101101111111), expected: i32(19) },
+ { input: i32Bits(0b00000000000000000011111111011111), expected: i32(18) },
+ { input: i32Bits(0b00000000000000000101111001110101), expected: i32(17) },
+ { input: i32Bits(0b00000000000000001101111011110111), expected: i32(16) },
+ { input: i32Bits(0b00000000000000011111111111110011), expected: i32(15) },
+ { input: i32Bits(0b00000000000000111111111110111111), expected: i32(14) },
+ { input: i32Bits(0b00000000000001111111011111111111), expected: i32(13) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(12) },
+ { input: i32Bits(0b00000000000111110101011110111111), expected: i32(11) },
+ { input: i32Bits(0b00000000001111101111111111110111), expected: i32(10) },
+ { input: i32Bits(0b00000000011111111111010000101111), expected: i32(9) },
+ { input: i32Bits(0b00000000111111111111001111111011), expected: i32(8) },
+ { input: i32Bits(0b00000001111111011111101111111111), expected: i32(7) },
+ { input: i32Bits(0b00000011101011111011110111111011), expected: i32(6) },
+ { input: i32Bits(0b00000111111110111111111111111111), expected: i32(5) },
+ { input: i32Bits(0b00001111000000011011011110111111), expected: i32(4) },
+ { input: i32Bits(0b00011110101111011111111111111111), expected: i32(3) },
+ { input: i32Bits(0b00110110111111100111111110111101), expected: i32(2) },
+ { input: i32Bits(0b01010111111101111111011111011111), expected: i32(1) },
+ { input: i32Bits(0b11100010011110101101101110101111), expected: i32(0) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts
new file mode 100644
index 0000000000..f0be916285
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countOneBits.spec.ts
@@ -0,0 +1,249 @@
+export const description = `
+Execution tests for the 'countOneBits' builtin function
+
+S is i32 or u32
+T is S or vecN<S>
+@const fn countOneBits(e: T ) -> T
+The number of 1 bits in the representation of e.
+Also known as "population count".
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeU32, u32Bits, u32, TypeI32, i32Bits, i32 } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countOneBits'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32(0) },
+
+ // One
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32(1) },
+
+ // 0's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32(1) },
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32(1) },
+
+ // 1's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000011), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000000111), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000001111), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000011111), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000000111111), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000001111111), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000000111111111), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000011111111111), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000000111111111111), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000001111111111111), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000011111111111111), expected: u32(14) },
+ { input: u32Bits(0b00000000000000000111111111111111), expected: u32(15) },
+ { input: u32Bits(0b00000000000000001111111111111111), expected: u32(16) },
+ { input: u32Bits(0b00000000000000011111111111111111), expected: u32(17) },
+ { input: u32Bits(0b00000000000000111111111111111111), expected: u32(18) },
+ { input: u32Bits(0b00000000000001111111111111111111), expected: u32(19) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(20) },
+ { input: u32Bits(0b00000000000111111111111111111111), expected: u32(21) },
+ { input: u32Bits(0b00000000001111111111111111111111), expected: u32(22) },
+ { input: u32Bits(0b00000000011111111111111111111111), expected: u32(23) },
+ { input: u32Bits(0b00000000111111111111111111111111), expected: u32(24) },
+ { input: u32Bits(0b00000001111111111111111111111111), expected: u32(25) },
+ { input: u32Bits(0b00000011111111111111111111111111), expected: u32(26) },
+ { input: u32Bits(0b00000111111111111111111111111111), expected: u32(27) },
+ { input: u32Bits(0b00001111111111111111111111111111), expected: u32(28) },
+ { input: u32Bits(0b00011111111111111111111111111111), expected: u32(29) },
+ { input: u32Bits(0b00111111111111111111111111111111), expected: u32(30) },
+ { input: u32Bits(0b01111111111111111111111111111111), expected: u32(31) },
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32(32) },
+
+ // random after leading 1
+ { input: u32Bits(0b00000000000000000000000000000110), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001101), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000011101), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000111001), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000001101111), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000000111101111), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000011111110001), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000111011011101), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000001101101111111), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000011111111011111), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000101111001110101), expected: u32(10) },
+ { input: u32Bits(0b00000000000000001101111011110111), expected: u32(13) },
+ { input: u32Bits(0b00000000000000011111111111110011), expected: u32(15) },
+ { input: u32Bits(0b00000000000000111111111110111111), expected: u32(17) },
+ { input: u32Bits(0b00000000000001111111011111111111), expected: u32(18) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(20) },
+ { input: u32Bits(0b00000000000111110101011110111111), expected: u32(17) },
+ { input: u32Bits(0b00000000001111101111111111110111), expected: u32(20) },
+ { input: u32Bits(0b00000000011111111111010000101111), expected: u32(17) },
+ { input: u32Bits(0b00000000111111111111001111111011), expected: u32(21) },
+ { input: u32Bits(0b00000001111111011111101111111111), expected: u32(23) },
+ { input: u32Bits(0b00000011101011111011110111111011), expected: u32(21) },
+ { input: u32Bits(0b00000111111110111111111111111111), expected: u32(26) },
+ { input: u32Bits(0b00001111000000011011011110111111), expected: u32(18) },
+ { input: u32Bits(0b00011110101111011111111111111111), expected: u32(26) },
+ { input: u32Bits(0b00110110111111100111111110111101), expected: u32(24) },
+ { input: u32Bits(0b01010111111101111111011111011111), expected: u32(26) },
+ { input: u32Bits(0b11100010011110101101101110101111), expected: u32(21) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countOneBits'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32(0) },
+
+ // One
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32(1) },
+
+ // 0's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32(1) },
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32(1) },
+
+ // 1's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000011), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000000111), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000001111), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000011111), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000000111111), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000001111111), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000000111111111), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000011111111111), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000000111111111111), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000001111111111111), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000011111111111111), expected: i32(14) },
+ { input: i32Bits(0b00000000000000000111111111111111), expected: i32(15) },
+ { input: i32Bits(0b00000000000000001111111111111111), expected: i32(16) },
+ { input: i32Bits(0b00000000000000011111111111111111), expected: i32(17) },
+ { input: i32Bits(0b00000000000000111111111111111111), expected: i32(18) },
+ { input: i32Bits(0b00000000000001111111111111111111), expected: i32(19) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(20) },
+ { input: i32Bits(0b00000000000111111111111111111111), expected: i32(21) },
+ { input: i32Bits(0b00000000001111111111111111111111), expected: i32(22) },
+ { input: i32Bits(0b00000000011111111111111111111111), expected: i32(23) },
+ { input: i32Bits(0b00000000111111111111111111111111), expected: i32(24) },
+ { input: i32Bits(0b00000001111111111111111111111111), expected: i32(25) },
+ { input: i32Bits(0b00000011111111111111111111111111), expected: i32(26) },
+ { input: i32Bits(0b00000111111111111111111111111111), expected: i32(27) },
+ { input: i32Bits(0b00001111111111111111111111111111), expected: i32(28) },
+ { input: i32Bits(0b00011111111111111111111111111111), expected: i32(29) },
+ { input: i32Bits(0b00111111111111111111111111111111), expected: i32(30) },
+ { input: i32Bits(0b01111111111111111111111111111111), expected: i32(31) },
+ { input: i32Bits(0b11111111111111111111111111111111), expected: i32(32) },
+
+ // random after leading 1
+ { input: i32Bits(0b00000000000000000000000000000110), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001101), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000011101), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000111001), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000001101111), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000000111101111), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000011111110001), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000111011011101), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000001101101111111), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000011111111011111), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000101111001110101), expected: i32(10) },
+ { input: i32Bits(0b00000000000000001101111011110111), expected: i32(13) },
+ { input: i32Bits(0b00000000000000011111111111110011), expected: i32(15) },
+ { input: i32Bits(0b00000000000000111111111110111111), expected: i32(17) },
+ { input: i32Bits(0b00000000000001111111011111111111), expected: i32(18) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(20) },
+ { input: i32Bits(0b00000000000111110101011110111111), expected: i32(17) },
+ { input: i32Bits(0b00000000001111101111111111110111), expected: i32(20) },
+ { input: i32Bits(0b00000000011111111111010000101111), expected: i32(17) },
+ { input: i32Bits(0b00000000111111111111001111111011), expected: i32(21) },
+ { input: i32Bits(0b00000001111111011111101111111111), expected: i32(23) },
+ { input: i32Bits(0b00000011101011111011110111111011), expected: i32(21) },
+ { input: i32Bits(0b00000111111110111111111111111111), expected: i32(26) },
+ { input: i32Bits(0b00001111000000011011011110111111), expected: i32(18) },
+ { input: i32Bits(0b00011110101111011111111111111111), expected: i32(26) },
+ { input: i32Bits(0b00110110111111100111111110111101), expected: i32(24) },
+ { input: i32Bits(0b01010111111101111111011111011111), expected: i32(26) },
+ { input: i32Bits(0b11100010011110101101101110101111), expected: i32(21) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts
new file mode 100644
index 0000000000..d0b3198f49
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/countTrailingZeros.spec.ts
@@ -0,0 +1,250 @@
+export const description = `
+Execution tests for the 'countTrailingZeros' builtin function
+
+S is i32 or u32
+T is S or vecN<S>
+@const fn countTrailingZeros(e: T ) -> T
+The number of consecutive 0 bits starting from the least significant bit of e,
+when T is a scalar type.
+Component-wise when T is a vector.
+Also known as "ctz" in some languages.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countTrailingZeros'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32(32) },
+
+ // High bit
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32(31) },
+
+ // 0's before trailing 1
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32(0) },
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32(14) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32(15) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32(16) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32(29) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32(30) },
+
+ // 1's before trailing 1
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32(0) },
+ { input: u32Bits(0b11111111111111111111111111111110), expected: u32(1) },
+ { input: u32Bits(0b11111111111111111111111111111100), expected: u32(2) },
+ { input: u32Bits(0b11111111111111111111111111111000), expected: u32(3) },
+ { input: u32Bits(0b11111111111111111111111111110000), expected: u32(4) },
+ { input: u32Bits(0b11111111111111111111111111100000), expected: u32(5) },
+ { input: u32Bits(0b11111111111111111111111111000000), expected: u32(6) },
+ { input: u32Bits(0b11111111111111111111111110000000), expected: u32(7) },
+ { input: u32Bits(0b11111111111111111111111100000000), expected: u32(8) },
+ { input: u32Bits(0b11111111111111111111111000000000), expected: u32(9) },
+ { input: u32Bits(0b11111111111111111111110000000000), expected: u32(10) },
+ { input: u32Bits(0b11111111111111111111100000000000), expected: u32(11) },
+ { input: u32Bits(0b11111111111111111111000000000000), expected: u32(12) },
+ { input: u32Bits(0b11111111111111111110000000000000), expected: u32(13) },
+ { input: u32Bits(0b11111111111111111100000000000000), expected: u32(14) },
+ { input: u32Bits(0b11111111111111111000000000000000), expected: u32(15) },
+ { input: u32Bits(0b11111111111111110000000000000000), expected: u32(16) },
+ { input: u32Bits(0b11111111111111100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b11111111111111000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b11111111111110000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b11111111111100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b11111111111000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b11111111110000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b11111111100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b11111111000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b11111110000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b11111100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b11111000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b11110000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b11100000000000000000000000000000), expected: u32(29) },
+ { input: u32Bits(0b11000000000000000000000000000000), expected: u32(30) },
+
+ // random before trailing 1
+ { input: u32Bits(0b11110000001111111101111010001111), expected: u32(0) },
+ { input: u32Bits(0b11011110111111100101110011110010), expected: u32(1) },
+ { input: u32Bits(0b11110111011011111111010000111100), expected: u32(2) },
+ { input: u32Bits(0b11010011011101111111010011101000), expected: u32(3) },
+ { input: u32Bits(0b11010111110111110001111110110000), expected: u32(4) },
+ { input: u32Bits(0b11111101111101111110101111100000), expected: u32(5) },
+ { input: u32Bits(0b11111001111011111001111011000000), expected: u32(6) },
+ { input: u32Bits(0b11001110110111110111111010000000), expected: u32(7) },
+ { input: u32Bits(0b11101111011111101110101100000000), expected: u32(8) },
+ { input: u32Bits(0b11111101111011111111111000000000), expected: u32(9) },
+ { input: u32Bits(0b10011111011101110110110000000000), expected: u32(10) },
+ { input: u32Bits(0b11111111101101111011100000000000), expected: u32(11) },
+ { input: u32Bits(0b11111011010110111011000000000000), expected: u32(12) },
+ { input: u32Bits(0b00111101010000111010000000000000), expected: u32(13) },
+ { input: u32Bits(0b11111011110001101100000000000000), expected: u32(14) },
+ { input: u32Bits(0b10111111010111111000000000000000), expected: u32(15) },
+ { input: u32Bits(0b11011101111010110000000000000000), expected: u32(16) },
+ { input: u32Bits(0b01110100110110100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b11100111001011000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b11111001110110000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b00110100100100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b11111010011000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b00000010110000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b11100111100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b00101101000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b11011010000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b11010100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b10111000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b01110000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b10100000000000000000000000000000), expected: u32(29) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('countTrailingZeros'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32(32) },
+
+ // High bit
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32(31) },
+
+ // 0's before trailing 1
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32(0) },
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32(14) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32(15) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32(16) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32(30) },
+
+ // 1's before trailing 1
+ { input: i32Bits(0b11111111111111111111111111111111), expected: i32(0) },
+ { input: i32Bits(0b11111111111111111111111111111110), expected: i32(1) },
+ { input: i32Bits(0b11111111111111111111111111111100), expected: i32(2) },
+ { input: i32Bits(0b11111111111111111111111111111000), expected: i32(3) },
+ { input: i32Bits(0b11111111111111111111111111110000), expected: i32(4) },
+ { input: i32Bits(0b11111111111111111111111111100000), expected: i32(5) },
+ { input: i32Bits(0b11111111111111111111111111000000), expected: i32(6) },
+ { input: i32Bits(0b11111111111111111111111110000000), expected: i32(7) },
+ { input: i32Bits(0b11111111111111111111111100000000), expected: i32(8) },
+ { input: i32Bits(0b11111111111111111111111000000000), expected: i32(9) },
+ { input: i32Bits(0b11111111111111111111110000000000), expected: i32(10) },
+ { input: i32Bits(0b11111111111111111111100000000000), expected: i32(11) },
+ { input: i32Bits(0b11111111111111111111000000000000), expected: i32(12) },
+ { input: i32Bits(0b11111111111111111110000000000000), expected: i32(13) },
+ { input: i32Bits(0b11111111111111111100000000000000), expected: i32(14) },
+ { input: i32Bits(0b11111111111111111000000000000000), expected: i32(15) },
+ { input: i32Bits(0b11111111111111110000000000000000), expected: i32(16) },
+ { input: i32Bits(0b11111111111111100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b11111111111111000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b11111111111110000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b11111111111100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b11111111111000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b11111111110000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b11111111100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b11111111000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b11111110000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b11111100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b11111000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b11110000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b11100000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b11000000000000000000000000000000), expected: i32(30) },
+
+ // random before trailing 1
+ { input: i32Bits(0b11110000001111111101111010001111), expected: i32(0) },
+ { input: i32Bits(0b11011110111111100101110011110010), expected: i32(1) },
+ { input: i32Bits(0b11110111011011111111010000111100), expected: i32(2) },
+ { input: i32Bits(0b11010011011101111111010011101000), expected: i32(3) },
+ { input: i32Bits(0b11010111110111110001111110110000), expected: i32(4) },
+ { input: i32Bits(0b11111101111101111110101111100000), expected: i32(5) },
+ { input: i32Bits(0b11111001111011111001111011000000), expected: i32(6) },
+ { input: i32Bits(0b11001110110111110111111010000000), expected: i32(7) },
+ { input: i32Bits(0b11101111011111101110101100000000), expected: i32(8) },
+ { input: i32Bits(0b11111101111011111111111000000000), expected: i32(9) },
+ { input: i32Bits(0b10011111011101110110110000000000), expected: i32(10) },
+ { input: i32Bits(0b11111111101101111011100000000000), expected: i32(11) },
+ { input: i32Bits(0b11111011010110111011000000000000), expected: i32(12) },
+ { input: i32Bits(0b00111101010000111010000000000000), expected: i32(13) },
+ { input: i32Bits(0b11111011110001101100000000000000), expected: i32(14) },
+ { input: i32Bits(0b10111111010111111000000000000000), expected: i32(15) },
+ { input: i32Bits(0b11011101111010110000000000000000), expected: i32(16) },
+ { input: i32Bits(0b01110100110110100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b11100111001011000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b11111001110110000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b00110100100100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b11111010011000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b00000010110000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b11100111100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b00101101000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b11011010000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b11010100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b10111000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b01110000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b10100000000000000000000000000000), expected: i32(29) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts
new file mode 100644
index 0000000000..5bce44049e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/cross.spec.ts
@@ -0,0 +1,66 @@
+export const description = `
+Execution tests for the 'cross' builtin function
+
+T is AbstractFloat, f32, or f16
+@const fn cross(e1: vec3<T> ,e2: vec3<T>) -> vec3<T>
+Returns the cross product of e1 and e2.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { crossInterval } from '../../../../../util/f32_interval.js';
+import { vectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateVectorPairToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('cross', {
+ f32_const: () => {
+ return generateVectorPairToVectorCases(
+ vectorF32Range(3),
+ vectorF32Range(3),
+ 'f32-only',
+ crossInterval
+ );
+ },
+ f32_non_const: () => {
+ return generateVectorPairToVectorCases(
+ vectorF32Range(3),
+ vectorF32Range(3),
+ 'unfiltered',
+ crossInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(
+ t,
+ builtin('cross'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32)],
+ TypeVec(3, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts
new file mode 100644
index 0000000000..9b4346408b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/degrees.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'degrees' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<T>
+@const fn degrees(e1: T ) -> T
+Converts radians to degrees, approximating e1 × 180 ÷ π. Component-wise when T is a vector
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { degreesInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('degrees', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'f32-only', degreesInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', degreesInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('degrees'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts
new file mode 100644
index 0000000000..6ea24ceb6f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/determinant.spec.ts
@@ -0,0 +1,32 @@
+export const description = `
+Execution tests for the 'determinant' builtin function
+
+T is AbstractFloat, f32, or f16
+@const determinant(e: matCxC<T> ) -> T
+Returns the determinant of e.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('dimension', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('dimension', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('dimension', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts
new file mode 100644
index 0000000000..f6c2239a02
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/distance.spec.ts
@@ -0,0 +1,172 @@
+export const description = `
+Execution tests for the 'distance' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn distance(e1: T ,e2: T ) -> f32
+Returns the distance between e1 and e2 (e.g. length(e1-e2)).
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { distanceInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, sparseVectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import {
+ allInputSources,
+ generateBinaryToF32IntervalCases,
+ generateVectorPairToF32IntervalCases,
+ run,
+} from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('distance', {
+ f32_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ distanceInterval
+ );
+ },
+ f32_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ distanceInterval
+ );
+ },
+ f32_vec2_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'f32-only',
+ distanceInterval
+ );
+ },
+ f32_vec2_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'unfiltered',
+ distanceInterval
+ );
+ },
+ f32_vec3_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'f32-only',
+ distanceInterval
+ );
+ },
+ f32_vec3_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'unfiltered',
+ distanceInterval
+ );
+ },
+ f32_vec4_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'f32-only',
+ distanceInterval
+ );
+ },
+ f32_vec4_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'unfiltered',
+ distanceInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('distance'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(
+ t,
+ builtin('distance'),
+ [TypeVec(2, TypeF32), TypeVec(2, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(
+ t,
+ builtin('distance'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(
+ t,
+ builtin('distance'),
+ [TypeVec(4, TypeF32), TypeVec(4, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts
new file mode 100644
index 0000000000..e76b4c56a9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dot.spec.ts
@@ -0,0 +1,156 @@
+export const description = `
+Execution tests for the 'dot' builtin function
+
+T is AbstractInt, AbstractFloat, i32, u32, f32, or f16
+@const fn dot(e1: vecN<T>,e2: vecN<T>) -> T
+Returns the dot product of e1 and e2.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { dotInterval } from '../../../../../util/f32_interval.js';
+import { sparseVectorF32Range, vectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateVectorPairToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// vec3 and vec4 require calculating all possible permutations, so their runtime is much longer per test, so only using
+// sparse vectors for them
+export const d = makeCaseCache('dot', {
+ f32_vec2_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ vectorF32Range(2),
+ vectorF32Range(2),
+ 'f32-only',
+ dotInterval
+ );
+ },
+ f32_vec2_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ vectorF32Range(2),
+ vectorF32Range(2),
+ 'unfiltered',
+ dotInterval
+ );
+ },
+ f32_vec3_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'f32-only',
+ dotInterval
+ );
+ },
+ f32_vec3_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'unfiltered',
+ dotInterval
+ );
+ },
+ f32_vec4_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'f32-only',
+ dotInterval
+ );
+ },
+ f32_vec4_non_const: () => {
+ return generateVectorPairToF32IntervalCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'unfiltered',
+ dotInterval
+ );
+ },
+});
+
+g.test('abstract_int')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`abstract int tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`abstract float test`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(
+ t,
+ builtin('dot'),
+ [TypeVec(2, TypeF32), TypeVec(2, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(
+ t,
+ builtin('dot'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(
+ t,
+ builtin('dot'),
+ [TypeVec(4, TypeF32), TypeVec(4, TypeF32)],
+ TypeF32,
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#vector-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts
new file mode 100644
index 0000000000..287a51c699
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdx.spec.ts
@@ -0,0 +1,23 @@
+export const description = `
+Execution tests for the 'dpdx' builtin function
+
+T is f32 or vecN<f32>
+fn dpdx(e:T) -> T
+Partial derivative of e with respect to window x coordinates.
+The result is the same as either dpdxFine(e) or dpdxCoarse(e).
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts
new file mode 100644
index 0000000000..67a75bb010
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxCoarse.spec.ts
@@ -0,0 +1,22 @@
+export const description = `
+Execution tests for the 'dpdxCoarse' builtin function
+
+T is f32 or vecN<f32>
+fn dpdxCoarse(e:T) ->T
+Returns the partial derivative of e with respect to window x coordinates using local differences.
+This may result in fewer unique positions that dpdxFine(e).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts
new file mode 100644
index 0000000000..91d65b990b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdxFine.spec.ts
@@ -0,0 +1,21 @@
+export const description = `
+Execution tests for the 'dpdxFine' builtin function
+
+T is f32 or vecN<f32>
+fn dpdxFine(e:T) ->T
+Returns the partial derivative of e with respect to window x coordinates.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts
new file mode 100644
index 0000000000..0cd9cafdb9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdy.spec.ts
@@ -0,0 +1,22 @@
+export const description = `
+Execution tests for the 'dpdy' builtin function
+
+T is f32 or vecN<f32>
+fn dpdy(e:T) ->T
+Partial derivative of e with respect to window y coordinates.
+The result is the same as either dpdyFine(e) or dpdyCoarse(e).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts
new file mode 100644
index 0000000000..f06869fdc2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyCoarse.spec.ts
@@ -0,0 +1,22 @@
+export const description = `
+Execution tests for the 'dpdyCoarse' builtin function
+
+T is f32 or vecN<f32>
+fn dpdyCoarse(e:T) ->T
+Returns the partial derivative of e with respect to window y coordinates using local differences.
+This may result in fewer unique positions that dpdyFine(e).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 test`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts
new file mode 100644
index 0000000000..e09761de95
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/dpdyFine.spec.ts
@@ -0,0 +1,21 @@
+export const description = `
+Execution tests for the 'dpdyFine' builtin function
+
+T is f32 or vecN<f32>
+fn dpdyFine(e:T) ->T
+Returns the partial derivative of e with respect to window y coordinates.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts
new file mode 100644
index 0000000000..1f43ac9d53
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp.spec.ts
@@ -0,0 +1,68 @@
+export const description = `
+Execution tests for the 'exp' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn exp(e1: T ) -> T
+Returns the natural exponentiation of e1 (e.g. e^e1). Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { expInterval } from '../../../../../util/f32_interval.js';
+import { biasedRange, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// floor(ln(max f32 value)) = 88, so exp(88) will be within range of a f32, but exp(89) will not
+// floor(ln(max f64 value)) = 709, so exp(709) can be handled by the testing framework, but exp(710) will misbehave
+const inputs = [
+ 0, // Returns 1 by definition
+ -89, // Returns subnormal value
+ kValue.f32.negative.min, // Closest to returning 0 as possible
+ ...biasedRange(kValue.f32.negative.max, -88, 100),
+ ...biasedRange(kValue.f32.positive.min, 88, 100),
+ ...linearRange(89, 709, 10), // Overflows f32, but not f64
+];
+
+export const d = makeCaseCache('exp', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', expInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', expInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('exp'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts
new file mode 100644
index 0000000000..e0321387c1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/exp2.spec.ts
@@ -0,0 +1,68 @@
+export const description = `
+Execution tests for the 'exp2' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn exp2(e: T ) -> T
+Returns 2 raised to the power e (e.g. 2^e). Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { exp2Interval } from '../../../../../util/f32_interval.js';
+import { biasedRange, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// floor(log2(max f32 value)) = 127, so exp2(127) will be within range of a f32, but exp2(128) will not
+// floor(ln(max f64 value)) = 1023, so exp2(1023) can be handled by the testing framework, but exp2(1024) will misbehave
+const inputs = [
+ 0, // Returns 1 by definition
+ -128, // Returns subnormal value
+ kValue.f32.negative.min, // Closest to returning 0 as possible
+ ...biasedRange(kValue.f32.negative.max, -127, 100),
+ ...biasedRange(kValue.f32.positive.min, 127, 100),
+ ...linearRange(128, 1023, 10), // Overflows f32, but not f64
+];
+
+export const d = makeCaseCache('exp2', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', exp2Interval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', exp2Interval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('exp2'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts
new file mode 100644
index 0000000000..d535bf5d74
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/extractBits.spec.ts
@@ -0,0 +1,337 @@
+export const description = `
+Execution tests for the 'extractBits' builtin function
+
+T is u32 or vecN<u32>
+@const fn extractBits(e: T, offset: u32, count: u32) -> T
+Reads bits from an integer, without sign extension.
+
+When T is a scalar type, then:
+ w is the bit width of T
+ o = min(offset,w)
+ c = min(count, w - o)
+
+The result is 0 if c is 0.
+Otherwise, bits 0..c-1 of the result are copied from bits o..o+c-1 of e.
+Other bits of the result are 0.
+Component-wise when T is a vector.
+
+
+T is i32 or vecN<i32>
+@const fn extractBits(e: T, offset: u32, count: u32) -> T
+Reads bits from an integer, with sign extension.
+
+When T is a scalar type, then:
+ w is the bit width of T
+ o = min(offset,w)
+ c = min(count, w - o)
+
+The result is 0 if c is 0.
+Otherwise, bits 0..c-1 of the result are copied from bits o..o+c-1 of e.
+Other bits of the result are the same as bit c-1 of the result.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ i32Bits,
+ TypeI32,
+ u32,
+ TypeU32,
+ u32Bits,
+ vec2,
+ vec3,
+ vec4,
+ TypeVec,
+} from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('width', [1, 2, 3, 4]))
+ .fn(async t => {
+ const cfg: Config = t.params;
+
+ const T = t.params.width === 1 ? TypeU32 : TypeVec(t.params.width, TypeU32);
+
+ const V = (x: number, y?: number, z?: number, w?: number) => {
+ y = y === undefined ? x : y;
+ z = z === undefined ? x : z;
+ w = w === undefined ? x : w;
+
+ switch (t.params.width) {
+ case 1:
+ return u32Bits(x);
+ case 2:
+ return vec2(u32Bits(x), u32Bits(y));
+ case 3:
+ return vec3(u32Bits(x), u32Bits(y), u32Bits(z));
+ default:
+ return vec4(u32Bits(x), u32Bits(y), u32Bits(z), u32Bits(w));
+ }
+ };
+
+ const all_1 = V(0b11111111111111111111111111111111);
+ const all_0 = V(0b00000000000000000000000000000000);
+ const low_1 = V(0b00000000000000000000000000000001);
+ const high_1 = V(0b10000000000000000000000000000000);
+ const pattern = V(
+ 0b00000000000111011100000000000000,
+ 0b11111111111000000011111111111111,
+ 0b00000000010101010101000000000000,
+ 0b00000000001010101010100000000000
+ );
+
+ const cases = [
+ { input: [all_0, u32(0), u32(32)], expected: all_0 },
+ { input: [all_0, u32(1), u32(10)], expected: all_0 },
+ { input: [all_0, u32(2), u32(5)], expected: all_0 },
+ { input: [all_0, u32(0), u32(1)], expected: all_0 },
+ { input: [all_0, u32(31), u32(1)], expected: all_0 },
+
+ { input: [all_1, u32(0), u32(32)], expected: all_1 },
+ {
+ input: [all_1, u32(1), u32(10)],
+ expected: V(0b00000000000000000000001111111111),
+ },
+ {
+ input: [all_1, u32(2), u32(5)],
+ expected: V(0b00000000000000000000000000011111),
+ },
+ { input: [all_1, u32(0), u32(1)], expected: low_1 },
+ { input: [all_1, u32(31), u32(1)], expected: low_1 },
+
+ // Patterns
+ { input: [pattern, u32(0), u32(32)], expected: pattern },
+ {
+ input: [pattern, u32(1), u32(31)],
+ expected: V(
+ 0b00000000000011101110000000000000,
+ 0b01111111111100000001111111111111,
+ 0b00000000001010101010100000000000,
+ 0b00000000000101010101010000000000
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(18)],
+ expected: V(
+ 0b00000000000000000000000001110111,
+ 0b00000000000000111111111110000000,
+ 0b00000000000000000000000101010101,
+ 0b00000000000000000000000010101010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(7)],
+ expected: V(
+ 0b00000000000000000000000001110111,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000001010101,
+ 0b00000000000000000000000000101010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(4)],
+ expected: V(
+ 0b00000000000000000000000000000111,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000101,
+ 0b00000000000000000000000000001010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(3)],
+ expected: V(
+ 0b00000000000000000000000000000111,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000101,
+ 0b00000000000000000000000000000010
+ ),
+ },
+ {
+ input: [pattern, u32(18), u32(3)],
+ expected: V(
+ 0b00000000000000000000000000000111,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000101,
+ 0b00000000000000000000000000000010
+ ),
+ },
+ { input: [low_1, u32(0), u32(1)], expected: low_1 },
+ { input: [high_1, u32(31), u32(1)], expected: low_1 },
+
+ // Zero count
+ { input: [all_1, u32(0), u32(0)], expected: all_0 },
+ { input: [all_0, u32(0), u32(0)], expected: all_0 },
+ { input: [low_1, u32(0), u32(0)], expected: all_0 },
+ { input: [high_1, u32(31), u32(0)], expected: all_0 },
+ { input: [pattern, u32(0), u32(0)], expected: all_0 },
+ ];
+
+ if (t.params.inputSource !== 'const') {
+ cases.push(
+ ...[
+ // End overflow
+ { input: [low_1, u32(0), u32(99)], expected: low_1 },
+ { input: [high_1, u32(31), u32(99)], expected: low_1 },
+ { input: [pattern, u32(0), u32(99)], expected: pattern },
+ {
+ input: [pattern, u32(14), u32(99)],
+ expected: V(
+ 0b00000000000000000000000001110111,
+ 0b00000000000000111111111110000000,
+ 0b00000000000000000000000101010101,
+ 0b00000000000000000000000010101010
+ ),
+ },
+ ]
+ );
+ }
+
+ await run(t, builtin('extractBits'), [T, TypeU32, TypeU32], T, cfg, cases);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('width', [1, 2, 3, 4]))
+ .fn(async t => {
+ const cfg: Config = t.params;
+
+ const T = t.params.width === 1 ? TypeI32 : TypeVec(t.params.width, TypeI32);
+
+ const V = (x: number, y?: number, z?: number, w?: number) => {
+ y = y === undefined ? x : y;
+ z = z === undefined ? x : z;
+ w = w === undefined ? x : w;
+
+ switch (t.params.width) {
+ case 1:
+ return i32Bits(x);
+ case 2:
+ return vec2(i32Bits(x), i32Bits(y));
+ case 3:
+ return vec3(i32Bits(x), i32Bits(y), i32Bits(z));
+ default:
+ return vec4(i32Bits(x), i32Bits(y), i32Bits(z), i32Bits(w));
+ }
+ };
+
+ const all_1 = V(0b11111111111111111111111111111111);
+ const all_0 = V(0b00000000000000000000000000000000);
+ const low_1 = V(0b00000000000000000000000000000001);
+ const high_1 = V(0b10000000000000000000000000000000);
+ const pattern = V(
+ 0b00000000000111011100000000000000,
+ 0b11111111111000000011111111111111,
+ 0b00000000010101010101000000000000,
+ 0b00000000001010101010100000000000
+ );
+
+ const cases = [
+ { input: [all_0, u32(0), u32(32)], expected: all_0 },
+ { input: [all_0, u32(1), u32(10)], expected: all_0 },
+ { input: [all_0, u32(2), u32(5)], expected: all_0 },
+ { input: [all_0, u32(0), u32(1)], expected: all_0 },
+ { input: [all_0, u32(31), u32(1)], expected: all_0 },
+
+ { input: [all_1, u32(0), u32(32)], expected: all_1 },
+ { input: [all_1, u32(1), u32(10)], expected: all_1 },
+ { input: [all_1, u32(2), u32(5)], expected: all_1 },
+ { input: [all_1, u32(0), u32(1)], expected: all_1 },
+ { input: [all_1, u32(31), u32(1)], expected: all_1 },
+
+ // Patterns
+ { input: [pattern, u32(0), u32(32)], expected: pattern },
+ {
+ input: [pattern, u32(1), u32(31)],
+ expected: V(
+ 0b00000000000011101110000000000000,
+ 0b11111111111100000001111111111111,
+ 0b00000000001010101010100000000000,
+ 0b00000000000101010101010000000000
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(18)],
+ expected: V(
+ 0b00000000000000000000000001110111,
+ 0b11111111111111111111111110000000,
+ 0b00000000000000000000000101010101,
+ 0b00000000000000000000000010101010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(7)],
+ expected: V(
+ 0b11111111111111111111111111110111,
+ 0b00000000000000000000000000000000,
+ 0b11111111111111111111111111010101,
+ 0b00000000000000000000000000101010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(4)],
+ expected: V(
+ 0b00000000000000000000000000000111,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000101,
+ 0b11111111111111111111111111111010
+ ),
+ },
+ {
+ input: [pattern, u32(14), u32(3)],
+ expected: V(
+ 0b11111111111111111111111111111111,
+ 0b00000000000000000000000000000000,
+ 0b11111111111111111111111111111101,
+ 0b00000000000000000000000000000010
+ ),
+ },
+ {
+ input: [pattern, u32(18), u32(3)],
+ expected: V(
+ 0b11111111111111111111111111111111,
+ 0b00000000000000000000000000000000,
+ 0b11111111111111111111111111111101,
+ 0b00000000000000000000000000000010
+ ),
+ },
+ { input: [low_1, u32(0), u32(1)], expected: all_1 },
+ { input: [high_1, u32(31), u32(1)], expected: all_1 },
+
+ // Zero count
+ { input: [all_1, u32(0), u32(0)], expected: all_0 },
+ { input: [all_0, u32(0), u32(0)], expected: all_0 },
+ { input: [low_1, u32(0), u32(0)], expected: all_0 },
+ { input: [high_1, u32(31), u32(0)], expected: all_0 },
+ { input: [pattern, u32(0), u32(0)], expected: all_0 },
+ ];
+
+ if (t.params.inputSource !== 'const') {
+ cases.push(
+ ...[
+ // End overflow
+ { input: [low_1, u32(0), u32(99)], expected: low_1 },
+ { input: [high_1, u32(31), u32(99)], expected: all_1 },
+ { input: [pattern, u32(0), u32(99)], expected: pattern },
+ {
+ input: [pattern, u32(14), u32(99)],
+ expected: V(
+ 0b00000000000000000000000001110111,
+ 0b11111111111111111111111110000000,
+ 0b00000000000000000000000101010101,
+ 0b00000000000000000000000010101010
+ ),
+ },
+ ]
+ );
+ }
+
+ await run(t, builtin('extractBits'), [T, TypeU32, TypeU32], T, cfg, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts
new file mode 100644
index 0000000000..22897f449a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/faceForward.spec.ts
@@ -0,0 +1,201 @@
+export const description = `
+Execution tests for the 'faceForward' builtin function
+
+T is vecN<AbstractFloat>, vecN<f32>, or vecN<f16>
+@const fn faceForward(e1: T ,e2: T ,e3: T ) -> T
+Returns e1 if dot(e2,e3) is negative, and -e1 otherwise.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { anyOf } from '../../../../../util/compare.js';
+import { f32, TypeF32, TypeVec, Vector } from '../../../../../util/conversion.js';
+import { F32Vector, faceForwardIntervals } from '../../../../../util/f32_interval.js';
+import { cartesianProduct, quantizeToF32, sparseVectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, IntervalFilter, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// Using a bespoke implementation of make*Case and generate*Cases here
+// since faceForwardIntervals is the only builtin with the API signature
+// (vec, vec, vec) -> vec
+//
+// Additionally faceForward has significant complexities around it due to the
+// fact that `dot` is calculated in it s operation, but the result of dot isn't
+// used to calculate the builtin's result.
+
+/**
+ * @returns a Case for `faceForward`
+ * @param x the `x` param for the case
+ * @param y the `y` param for the case
+ * @param z the `z` param for the case
+ * @param check what interval checking to apply
+ * */
+function makeCaseF32(
+ x: number[],
+ y: number[],
+ z: number[],
+ check: IntervalFilter
+): Case | undefined {
+ x = x.map(quantizeToF32);
+ y = y.map(quantizeToF32);
+ z = z.map(quantizeToF32);
+
+ const x_f32 = x.map(f32);
+ const y_f32 = y.map(f32);
+ const z_f32 = z.map(f32);
+
+ const results = faceForwardIntervals(x, y, z);
+ if (check === 'f32-only' && results.some(r => r === undefined)) {
+ return undefined;
+ }
+
+ // Stripping the undefined results, since undefined is used to signal that an OOB
+ // could occur within the calculation that isn't reflected in the result
+ // intervals.
+ const define_results = results.filter((r): r is F32Vector => r !== undefined);
+
+ return {
+ input: [new Vector(x_f32), new Vector(y_f32), new Vector(z_f32)],
+ expected: anyOf(...define_results),
+ };
+}
+
+/**
+ * @returns an array of Cases for `faceForward`
+ * @param xs array of inputs to try for the `x` param
+ * @param ys array of inputs to try for the `y` param
+ * @param zs array of inputs to try for the `z` param
+ * @param check what interval checking to apply
+ */
+function generateCasesF32(
+ xs: number[][],
+ ys: number[][],
+ zs: number[][],
+ check: IntervalFilter
+): Case[] {
+ // Cannot use `cartesianProduct` here due to heterogeneous param types
+ return cartesianProduct(xs, ys, zs)
+ .map(e => makeCaseF32(e[0], e[1], e[2], check))
+ .filter((c): c is Case => c !== undefined);
+}
+
+export const d = makeCaseCache('faceForward', {
+ f32_vec2_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'f32-only'
+ );
+ },
+ f32_vec2_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'unfiltered'
+ );
+ },
+ f32_vec3_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'f32-only'
+ );
+ },
+ f32_vec3_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'unfiltered'
+ );
+ },
+ f32_vec4_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'f32-only'
+ );
+ },
+ f32_vec4_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'unfiltered'
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(
+ t,
+ builtin('faceForward'),
+ [TypeVec(2, TypeF32), TypeVec(2, TypeF32), TypeVec(2, TypeF32)],
+ TypeVec(2, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(
+ t,
+ builtin('faceForward'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32), TypeVec(3, TypeF32)],
+ TypeVec(3, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(
+ t,
+ builtin('faceForward'),
+ [TypeVec(4, TypeF32), TypeVec(4, TypeF32), TypeVec(4, TypeF32)],
+ TypeVec(4, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts
new file mode 100644
index 0000000000..a04103304a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstLeadingBit.spec.ts
@@ -0,0 +1,347 @@
+export const description = `
+Execution tests for the 'firstLeadingBit' builtin function
+
+T is u32 or vecN<u32>
+@const fn firstLeadingBit(e: T ) -> T
+For scalar T, the result is: T(-1) if e is zero.
+Otherwise the position of the most significant 1 bit in e.
+Component-wise when T is a vector.
+
+T is i32 or vecN<i32>
+@const fn firstLeadingBit(e: T ) -> T
+For scalar T, the result is: -1 if e is 0 or -1.
+Otherwise the position of the most significant bit in e that is different from e’s sign bit.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('firstLeadingBit'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32(-1) },
+
+ // One
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32(0) },
+
+ // 0's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32(14) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32(15) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32(16) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32(29) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32(30) },
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32(31) },
+
+ // 1's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000011), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000000111), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001111), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000011111), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000111111), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000001111111), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000111111111), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000011111111111), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000111111111111), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000001111111111111), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000011111111111111), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000111111111111111), expected: u32(14) },
+ { input: u32Bits(0b00000000000000001111111111111111), expected: u32(15) },
+ { input: u32Bits(0b00000000000000011111111111111111), expected: u32(16) },
+ { input: u32Bits(0b00000000000000111111111111111111), expected: u32(17) },
+ { input: u32Bits(0b00000000000001111111111111111111), expected: u32(18) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(19) },
+ { input: u32Bits(0b00000000000111111111111111111111), expected: u32(20) },
+ { input: u32Bits(0b00000000001111111111111111111111), expected: u32(21) },
+ { input: u32Bits(0b00000000011111111111111111111111), expected: u32(22) },
+ { input: u32Bits(0b00000000111111111111111111111111), expected: u32(23) },
+ { input: u32Bits(0b00000001111111111111111111111111), expected: u32(24) },
+ { input: u32Bits(0b00000011111111111111111111111111), expected: u32(25) },
+ { input: u32Bits(0b00000111111111111111111111111111), expected: u32(26) },
+ { input: u32Bits(0b00001111111111111111111111111111), expected: u32(27) },
+ { input: u32Bits(0b00011111111111111111111111111111), expected: u32(28) },
+ { input: u32Bits(0b00111111111111111111111111111111), expected: u32(29) },
+ { input: u32Bits(0b01111111111111111111111111111111), expected: u32(30) },
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32(31) },
+
+ // random after leading 1
+ { input: u32Bits(0b00000000000000000000000000000110), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001101), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000011101), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000111001), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000001101111), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000111101111), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000011111110001), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000111011011101), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000001101101111111), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000011111111011111), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000101111001110101), expected: u32(14) },
+ { input: u32Bits(0b00000000000000001101111011110111), expected: u32(15) },
+ { input: u32Bits(0b00000000000000011111111111110011), expected: u32(16) },
+ { input: u32Bits(0b00000000000000111111111110111111), expected: u32(17) },
+ { input: u32Bits(0b00000000000001111111011111111111), expected: u32(18) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32(19) },
+ { input: u32Bits(0b00000000000111110101011110111111), expected: u32(20) },
+ { input: u32Bits(0b00000000001111101111111111110111), expected: u32(21) },
+ { input: u32Bits(0b00000000011111111111010000101111), expected: u32(22) },
+ { input: u32Bits(0b00000000111111111111001111111011), expected: u32(23) },
+ { input: u32Bits(0b00000001111111011111101111111111), expected: u32(24) },
+ { input: u32Bits(0b00000011101011111011110111111011), expected: u32(25) },
+ { input: u32Bits(0b00000111111110111111111111111111), expected: u32(26) },
+ { input: u32Bits(0b00001111000000011011011110111111), expected: u32(27) },
+ { input: u32Bits(0b00011110101111011111111111111111), expected: u32(28) },
+ { input: u32Bits(0b00110110111111100111111110111101), expected: u32(29) },
+ { input: u32Bits(0b01010111111101111111011111011111), expected: u32(30) },
+ { input: u32Bits(0b11100010011110101101101110101111), expected: u32(31) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('firstLeadingBit'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32(-1) },
+
+ // One
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32(0) },
+
+ // Positive: 0's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32(14) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32(15) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32(16) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32(30) },
+
+ // Negative: 0's after leading 0
+ { input: i32Bits(0b11111111111111111111111111111110), expected: i32(0) },
+ { input: i32Bits(0b11111111111111111111111111111100), expected: i32(1) },
+ { input: i32Bits(0b11111111111111111111111111111000), expected: i32(2) },
+ { input: i32Bits(0b11111111111111111111111111110000), expected: i32(3) },
+ { input: i32Bits(0b11111111111111111111111111100000), expected: i32(4) },
+ { input: i32Bits(0b11111111111111111111111111000000), expected: i32(5) },
+ { input: i32Bits(0b11111111111111111111111110000000), expected: i32(6) },
+ { input: i32Bits(0b11111111111111111111111100000000), expected: i32(7) },
+ { input: i32Bits(0b11111111111111111111111000000000), expected: i32(8) },
+ { input: i32Bits(0b11111111111111111111110000000000), expected: i32(9) },
+ { input: i32Bits(0b11111111111111111111100000000000), expected: i32(10) },
+ { input: i32Bits(0b11111111111111111111000000000000), expected: i32(11) },
+ { input: i32Bits(0b11111111111111111110000000000000), expected: i32(12) },
+ { input: i32Bits(0b11111111111111111100000000000000), expected: i32(13) },
+ { input: i32Bits(0b11111111111111111000000000000000), expected: i32(14) },
+ { input: i32Bits(0b11111111111111110000000000000000), expected: i32(15) },
+ { input: i32Bits(0b11111111111111100000000000000000), expected: i32(16) },
+ { input: i32Bits(0b11111111111111000000000000000000), expected: i32(17) },
+ { input: i32Bits(0b11111111111110000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b11111111111100000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b11111111111000000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b11111111110000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b11111111100000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b11111111000000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b11111110000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b11111100000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b11111000000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b11110000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b11100000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b11000000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32(30) },
+
+ // Positive: 1's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000011), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000000111), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001111), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000011111), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000111111), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000001111111), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000111111111), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000011111111111), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000111111111111), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000001111111111111), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000011111111111111), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000111111111111111), expected: i32(14) },
+ { input: i32Bits(0b00000000000000001111111111111111), expected: i32(15) },
+ { input: i32Bits(0b00000000000000011111111111111111), expected: i32(16) },
+ { input: i32Bits(0b00000000000000111111111111111111), expected: i32(17) },
+ { input: i32Bits(0b00000000000001111111111111111111), expected: i32(18) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(19) },
+ { input: i32Bits(0b00000000000111111111111111111111), expected: i32(20) },
+ { input: i32Bits(0b00000000001111111111111111111111), expected: i32(21) },
+ { input: i32Bits(0b00000000011111111111111111111111), expected: i32(22) },
+ { input: i32Bits(0b00000000111111111111111111111111), expected: i32(23) },
+ { input: i32Bits(0b00000001111111111111111111111111), expected: i32(24) },
+ { input: i32Bits(0b00000011111111111111111111111111), expected: i32(25) },
+ { input: i32Bits(0b00000111111111111111111111111111), expected: i32(26) },
+ { input: i32Bits(0b00001111111111111111111111111111), expected: i32(27) },
+ { input: i32Bits(0b00011111111111111111111111111111), expected: i32(28) },
+ { input: i32Bits(0b00111111111111111111111111111111), expected: i32(29) },
+ { input: i32Bits(0b01111111111111111111111111111111), expected: i32(30) },
+
+ // Negative: 1's after leading 0
+ { input: i32Bits(0b11111111111111111111111111111101), expected: i32(1) },
+ { input: i32Bits(0b11111111111111111111111111111011), expected: i32(2) },
+ { input: i32Bits(0b11111111111111111111111111110111), expected: i32(3) },
+ { input: i32Bits(0b11111111111111111111111111101111), expected: i32(4) },
+ { input: i32Bits(0b11111111111111111111111111011111), expected: i32(5) },
+ { input: i32Bits(0b11111111111111111111111110111111), expected: i32(6) },
+ { input: i32Bits(0b11111111111111111111111101111111), expected: i32(7) },
+ { input: i32Bits(0b11111111111111111111111011111111), expected: i32(8) },
+ { input: i32Bits(0b11111111111111111111110111111111), expected: i32(9) },
+ { input: i32Bits(0b11111111111111111111101111111111), expected: i32(10) },
+ { input: i32Bits(0b11111111111111111111011111111111), expected: i32(11) },
+ { input: i32Bits(0b11111111111111111110111111111111), expected: i32(12) },
+ { input: i32Bits(0b11111111111111111101111111111111), expected: i32(13) },
+ { input: i32Bits(0b11111111111111111011111111111111), expected: i32(14) },
+ { input: i32Bits(0b11111111111111110111111111111111), expected: i32(15) },
+ { input: i32Bits(0b11111111111111101111111111111111), expected: i32(16) },
+ { input: i32Bits(0b11111111111111011111111111111111), expected: i32(17) },
+ { input: i32Bits(0b11111111111110111111111111111111), expected: i32(18) },
+ { input: i32Bits(0b11111111111101111111111111111111), expected: i32(19) },
+ { input: i32Bits(0b11111111111011111111111111111111), expected: i32(20) },
+ { input: i32Bits(0b11111111110111111111111111111111), expected: i32(21) },
+ { input: i32Bits(0b11111111101111111111111111111111), expected: i32(22) },
+ { input: i32Bits(0b11111111011111111111111111111111), expected: i32(23) },
+ { input: i32Bits(0b11111110111111111111111111111111), expected: i32(24) },
+ { input: i32Bits(0b11111101111111111111111111111111), expected: i32(25) },
+ { input: i32Bits(0b11111011111111111111111111111111), expected: i32(26) },
+ { input: i32Bits(0b11110111111111111111111111111111), expected: i32(27) },
+ { input: i32Bits(0b11101111111111111111111111111111), expected: i32(28) },
+ { input: i32Bits(0b11011111111111111111111111111111), expected: i32(29) },
+ { input: i32Bits(0b10111111111111111111111111111111), expected: i32(30) },
+
+ // Positive: random after leading 1
+ { input: i32Bits(0b00000000000000000000000000000110), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001101), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000011101), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000111001), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000001101111), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000111101111), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000011111110001), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000111011011101), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000001101101111111), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000011111111011111), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000101111001110101), expected: i32(14) },
+ { input: i32Bits(0b00000000000000001101111011110111), expected: i32(15) },
+ { input: i32Bits(0b00000000000000011111111111110011), expected: i32(16) },
+ { input: i32Bits(0b00000000000000111111111110111111), expected: i32(17) },
+ { input: i32Bits(0b00000000000001111111011111111111), expected: i32(18) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32(19) },
+ { input: i32Bits(0b00000000000111110101011110111111), expected: i32(20) },
+ { input: i32Bits(0b00000000001111101111111111110111), expected: i32(21) },
+ { input: i32Bits(0b00000000011111111111010000101111), expected: i32(22) },
+ { input: i32Bits(0b00000000111111111111001111111011), expected: i32(23) },
+ { input: i32Bits(0b00000001111111011111101111111111), expected: i32(24) },
+ { input: i32Bits(0b00000011101011111011110111111011), expected: i32(25) },
+ { input: i32Bits(0b00000111111110111111111111111111), expected: i32(26) },
+ { input: i32Bits(0b00001111000000011011011110111111), expected: i32(27) },
+ { input: i32Bits(0b00011110101111011111111111111111), expected: i32(28) },
+ { input: i32Bits(0b00110110111111100111111110111101), expected: i32(29) },
+ { input: i32Bits(0b01010111111101111111011111011111), expected: i32(30) },
+
+ // Negative: random after leading 0
+ { input: i32Bits(0b11111111111111111111111111111010), expected: i32(2) },
+ { input: i32Bits(0b11111111111111111111111111110110), expected: i32(3) },
+ { input: i32Bits(0b11111111111111111111111111101101), expected: i32(4) },
+ { input: i32Bits(0b11111111111111111111111111011101), expected: i32(5) },
+ { input: i32Bits(0b11111111111111111111111110111001), expected: i32(6) },
+ { input: i32Bits(0b11111111111111111111111101101111), expected: i32(7) },
+ { input: i32Bits(0b11111111111111111111111011111111), expected: i32(8) },
+ { input: i32Bits(0b11111111111111111111110111101111), expected: i32(9) },
+ { input: i32Bits(0b11111111111111111111101111111111), expected: i32(10) },
+ { input: i32Bits(0b11111111111111111111011111110001), expected: i32(11) },
+ { input: i32Bits(0b11111111111111111110111011011101), expected: i32(12) },
+ { input: i32Bits(0b11111111111111111101101101111111), expected: i32(13) },
+ { input: i32Bits(0b11111111111111111011111111011111), expected: i32(14) },
+ { input: i32Bits(0b11111111111111110101111001110101), expected: i32(15) },
+ { input: i32Bits(0b11111111111111101101111011110111), expected: i32(16) },
+ { input: i32Bits(0b11111111111111011111111111110011), expected: i32(17) },
+ { input: i32Bits(0b11111111111110111111111110111111), expected: i32(18) },
+ { input: i32Bits(0b11111111111101111111011111111111), expected: i32(19) },
+ { input: i32Bits(0b11111111111011111111111111111111), expected: i32(20) },
+ { input: i32Bits(0b11111111110111110101011110111111), expected: i32(21) },
+ { input: i32Bits(0b11111111101111101111111111110111), expected: i32(22) },
+ { input: i32Bits(0b11111111011111111111010000101111), expected: i32(23) },
+ { input: i32Bits(0b11111110111111111111001111111011), expected: i32(24) },
+ { input: i32Bits(0b11111101111111011111101111111111), expected: i32(25) },
+ { input: i32Bits(0b11111011101011111011110111111011), expected: i32(26) },
+ { input: i32Bits(0b11110111111110111111111111111111), expected: i32(27) },
+ { input: i32Bits(0b11101111000000011011011110111111), expected: i32(28) },
+ { input: i32Bits(0b11011110101111011111111111111111), expected: i32(29) },
+ { input: i32Bits(0b10110110111111100111111110111101), expected: i32(30) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts
new file mode 100644
index 0000000000..5c65f59d28
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/firstTrailingBit.spec.ts
@@ -0,0 +1,250 @@
+export const description = `
+Execution tests for the 'firstTrailingBit' builtin function
+
+S is i32, u32
+T is S or vecN<S>
+@const fn firstTrailingBit(e: T ) -> T
+For scalar T, the result is: T(-1) if e is zero.
+Otherwise the position of the least significant 1 bit in e.
+Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { i32, i32Bits, TypeI32, u32, TypeU32, u32Bits } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('firstTrailingBit'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32(-1) },
+
+ // High bit
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32(31) },
+
+ // 0's before trailing 1
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32(0) },
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32(1) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32(2) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32(3) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32(4) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32(5) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32(6) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32(7) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32(8) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32(9) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32(10) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32(11) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32(12) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32(13) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32(14) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32(15) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32(16) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32(29) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32(30) },
+
+ // 1's before trailing 1
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32(0) },
+ { input: u32Bits(0b11111111111111111111111111111110), expected: u32(1) },
+ { input: u32Bits(0b11111111111111111111111111111100), expected: u32(2) },
+ { input: u32Bits(0b11111111111111111111111111111000), expected: u32(3) },
+ { input: u32Bits(0b11111111111111111111111111110000), expected: u32(4) },
+ { input: u32Bits(0b11111111111111111111111111100000), expected: u32(5) },
+ { input: u32Bits(0b11111111111111111111111111000000), expected: u32(6) },
+ { input: u32Bits(0b11111111111111111111111110000000), expected: u32(7) },
+ { input: u32Bits(0b11111111111111111111111100000000), expected: u32(8) },
+ { input: u32Bits(0b11111111111111111111111000000000), expected: u32(9) },
+ { input: u32Bits(0b11111111111111111111110000000000), expected: u32(10) },
+ { input: u32Bits(0b11111111111111111111100000000000), expected: u32(11) },
+ { input: u32Bits(0b11111111111111111111000000000000), expected: u32(12) },
+ { input: u32Bits(0b11111111111111111110000000000000), expected: u32(13) },
+ { input: u32Bits(0b11111111111111111100000000000000), expected: u32(14) },
+ { input: u32Bits(0b11111111111111111000000000000000), expected: u32(15) },
+ { input: u32Bits(0b11111111111111110000000000000000), expected: u32(16) },
+ { input: u32Bits(0b11111111111111100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b11111111111111000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b11111111111110000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b11111111111100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b11111111111000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b11111111110000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b11111111100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b11111111000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b11111110000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b11111100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b11111000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b11110000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b11100000000000000000000000000000), expected: u32(29) },
+ { input: u32Bits(0b11000000000000000000000000000000), expected: u32(30) },
+
+ // random before trailing 1
+ { input: u32Bits(0b11110000001111111101111010001111), expected: u32(0) },
+ { input: u32Bits(0b11011110111111100101110011110010), expected: u32(1) },
+ { input: u32Bits(0b11110111011011111111010000111100), expected: u32(2) },
+ { input: u32Bits(0b11010011011101111111010011101000), expected: u32(3) },
+ { input: u32Bits(0b11010111110111110001111110110000), expected: u32(4) },
+ { input: u32Bits(0b11111101111101111110101111100000), expected: u32(5) },
+ { input: u32Bits(0b11111001111011111001111011000000), expected: u32(6) },
+ { input: u32Bits(0b11001110110111110111111010000000), expected: u32(7) },
+ { input: u32Bits(0b11101111011111101110101100000000), expected: u32(8) },
+ { input: u32Bits(0b11111101111011111111111000000000), expected: u32(9) },
+ { input: u32Bits(0b10011111011101110110110000000000), expected: u32(10) },
+ { input: u32Bits(0b11111111101101111011100000000000), expected: u32(11) },
+ { input: u32Bits(0b11111011010110111011000000000000), expected: u32(12) },
+ { input: u32Bits(0b00111101010000111010000000000000), expected: u32(13) },
+ { input: u32Bits(0b11111011110001101100000000000000), expected: u32(14) },
+ { input: u32Bits(0b10111111010111111000000000000000), expected: u32(15) },
+ { input: u32Bits(0b11011101111010110000000000000000), expected: u32(16) },
+ { input: u32Bits(0b01110100110110100000000000000000), expected: u32(17) },
+ { input: u32Bits(0b11100111001011000000000000000000), expected: u32(18) },
+ { input: u32Bits(0b11111001110110000000000000000000), expected: u32(19) },
+ { input: u32Bits(0b00110100100100000000000000000000), expected: u32(20) },
+ { input: u32Bits(0b11111010011000000000000000000000), expected: u32(21) },
+ { input: u32Bits(0b00000010110000000000000000000000), expected: u32(22) },
+ { input: u32Bits(0b11100111100000000000000000000000), expected: u32(23) },
+ { input: u32Bits(0b00101101000000000000000000000000), expected: u32(24) },
+ { input: u32Bits(0b11011010000000000000000000000000), expected: u32(25) },
+ { input: u32Bits(0b11010100000000000000000000000000), expected: u32(26) },
+ { input: u32Bits(0b10111000000000000000000000000000), expected: u32(27) },
+ { input: u32Bits(0b01110000000000000000000000000000), expected: u32(28) },
+ { input: u32Bits(0b10100000000000000000000000000000), expected: u32(29) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ await run(t, builtin('firstTrailingBit'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32(-1) },
+
+ // High bit
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32(31) },
+
+ // 0's before trailing 1
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32(0) },
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32(1) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32(2) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32(3) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32(4) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32(5) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32(6) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32(7) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32(8) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32(9) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32(10) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32(11) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32(12) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32(13) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32(14) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32(15) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32(16) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32(30) },
+
+ // 1's before trailing 1
+ { input: i32Bits(0b11111111111111111111111111111111), expected: i32(0) },
+ { input: i32Bits(0b11111111111111111111111111111110), expected: i32(1) },
+ { input: i32Bits(0b11111111111111111111111111111100), expected: i32(2) },
+ { input: i32Bits(0b11111111111111111111111111111000), expected: i32(3) },
+ { input: i32Bits(0b11111111111111111111111111110000), expected: i32(4) },
+ { input: i32Bits(0b11111111111111111111111111100000), expected: i32(5) },
+ { input: i32Bits(0b11111111111111111111111111000000), expected: i32(6) },
+ { input: i32Bits(0b11111111111111111111111110000000), expected: i32(7) },
+ { input: i32Bits(0b11111111111111111111111100000000), expected: i32(8) },
+ { input: i32Bits(0b11111111111111111111111000000000), expected: i32(9) },
+ { input: i32Bits(0b11111111111111111111110000000000), expected: i32(10) },
+ { input: i32Bits(0b11111111111111111111100000000000), expected: i32(11) },
+ { input: i32Bits(0b11111111111111111111000000000000), expected: i32(12) },
+ { input: i32Bits(0b11111111111111111110000000000000), expected: i32(13) },
+ { input: i32Bits(0b11111111111111111100000000000000), expected: i32(14) },
+ { input: i32Bits(0b11111111111111111000000000000000), expected: i32(15) },
+ { input: i32Bits(0b11111111111111110000000000000000), expected: i32(16) },
+ { input: i32Bits(0b11111111111111100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b11111111111111000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b11111111111110000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b11111111111100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b11111111111000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b11111111110000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b11111111100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b11111111000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b11111110000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b11111100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b11111000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b11110000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b11100000000000000000000000000000), expected: i32(29) },
+ { input: i32Bits(0b11000000000000000000000000000000), expected: i32(30) },
+
+ // random before trailing 1
+ { input: i32Bits(0b11110000001111111101111010001111), expected: i32(0) },
+ { input: i32Bits(0b11011110111111100101110011110010), expected: i32(1) },
+ { input: i32Bits(0b11110111011011111111010000111100), expected: i32(2) },
+ { input: i32Bits(0b11010011011101111111010011101000), expected: i32(3) },
+ { input: i32Bits(0b11010111110111110001111110110000), expected: i32(4) },
+ { input: i32Bits(0b11111101111101111110101111100000), expected: i32(5) },
+ { input: i32Bits(0b11111001111011111001111011000000), expected: i32(6) },
+ { input: i32Bits(0b11001110110111110111111010000000), expected: i32(7) },
+ { input: i32Bits(0b11101111011111101110101100000000), expected: i32(8) },
+ { input: i32Bits(0b11111101111011111111111000000000), expected: i32(9) },
+ { input: i32Bits(0b10011111011101110110110000000000), expected: i32(10) },
+ { input: i32Bits(0b11111111101101111011100000000000), expected: i32(11) },
+ { input: i32Bits(0b11111011010110111011000000000000), expected: i32(12) },
+ { input: i32Bits(0b00111101010000111010000000000000), expected: i32(13) },
+ { input: i32Bits(0b11111011110001101100000000000000), expected: i32(14) },
+ { input: i32Bits(0b10111111010111111000000000000000), expected: i32(15) },
+ { input: i32Bits(0b11011101111010110000000000000000), expected: i32(16) },
+ { input: i32Bits(0b01110100110110100000000000000000), expected: i32(17) },
+ { input: i32Bits(0b11100111001011000000000000000000), expected: i32(18) },
+ { input: i32Bits(0b11111001110110000000000000000000), expected: i32(19) },
+ { input: i32Bits(0b00110100100100000000000000000000), expected: i32(20) },
+ { input: i32Bits(0b11111010011000000000000000000000), expected: i32(21) },
+ { input: i32Bits(0b00000010110000000000000000000000), expected: i32(22) },
+ { input: i32Bits(0b11100111100000000000000000000000), expected: i32(23) },
+ { input: i32Bits(0b00101101000000000000000000000000), expected: i32(24) },
+ { input: i32Bits(0b11011010000000000000000000000000), expected: i32(25) },
+ { input: i32Bits(0b11010100000000000000000000000000), expected: i32(26) },
+ { input: i32Bits(0b10111000000000000000000000000000), expected: i32(27) },
+ { input: i32Bits(0b01110000000000000000000000000000), expected: i32(28) },
+ { input: i32Bits(0b10100000000000000000000000000000), expected: i32(29) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts
new file mode 100644
index 0000000000..227efff7ef
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/floor.spec.ts
@@ -0,0 +1,71 @@
+export const description = `
+Execution tests for the 'floor' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn floor(e: T ) -> T
+Returns the floor of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { floorInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('floor', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Small positive numbers
+ 0.1,
+ 0.9,
+ 1.0,
+ 1.1,
+ 1.9,
+ // Small negative numbers
+ -0.1,
+ -0.9,
+ -1.0,
+ -1.1,
+ -1.9,
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ floorInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('floor'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts
new file mode 100644
index 0000000000..f7488eb7b0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fma.spec.ts
@@ -0,0 +1,68 @@
+export const description = `
+Execution tests for the 'fma' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn fma(e1: T ,e2: T ,e3: T ) -> T
+Returns e1 * e2 + e3. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { fmaInterval } from '../../../../../util/f32_interval.js';
+import { sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateTernaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('fma', {
+ f32_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'f32-only',
+ fmaInterval
+ );
+ },
+ f32_non_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'unfiltered',
+ fmaInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('fma'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts
new file mode 100644
index 0000000000..c7d0244b96
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fract.spec.ts
@@ -0,0 +1,73 @@
+export const description = `
+Execution tests for the 'fract' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn fract(e: T ) -> T
+Returns the fractional part of e, computed as e - floor(e).
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { fractInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('fract', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ 0.5, // 0.5 -> 0.5
+ 0.9, // ~0.9 -> ~0.9
+ 1, // 1 -> 0
+ 2, // 2 -> 0
+ 1.11, // ~1.11 -> ~0.11
+ 10.0001, // ~10.0001 -> ~0.0001
+ -0.1, // ~-0.1 -> ~0.9
+ -0.5, // -0.5 -> 0.5
+ -0.9, // ~-0.9 -> ~0.1
+ -1, // -1 -> 0
+ -2, // -2 -> 0
+ -1.11, // ~-1.11 -> ~0.89
+ -10.0001, // -10.0001 -> ~0.9999
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ fractInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('fract'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts
new file mode 100644
index 0000000000..02f301f868
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/frexp.spec.ts
@@ -0,0 +1,80 @@
+export const description = `
+Execution tests for the 'frexp' builtin function
+
+S is f32 or f16
+T is S or vecN<S>
+
+@const fn frexp(e: T) -> result_struct
+
+Splits e into a significand and exponent of the form significand * 2^exponent.
+Returns the result_struct for the appropriate overload.
+
+
+The magnitude of the significand is in the range of [0.5, 1.0) or 0.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('scalar_f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+struct __frexp_result {
+ sig : f32, // significand part
+ exp : i32 // exponent part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('scalar_f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f16 tests
+
+struct __frexp_result_f16 {
+ sig : f16, // significand part
+ exp : i32 // exponent part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('vector_f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+vecN<f32>
+
+struct __frexp_result_vecN {
+ sig : vecN<f32>, // significand part
+ exp : vecN<i32> // exponent part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('vector_f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+vecN<f16>
+
+struct __frexp_result_vecN_f16 {
+ sig : vecN<f16>, // significand part
+ exp : vecN<i32> // exponent part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidth.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidth.spec.ts
new file mode 100644
index 0000000000..7c6f0232a9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidth.spec.ts
@@ -0,0 +1,21 @@
+export const description = `
+Execution tests for the 'fwidth' builtin function
+
+T is f32 or vecN<f32>
+fn fwidth(e:T) ->T
+Returns abs(dpdx(e)) + abs(dpdy(e)).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthCoarse.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthCoarse.spec.ts
new file mode 100644
index 0000000000..9f93237934
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthCoarse.spec.ts
@@ -0,0 +1,21 @@
+export const description = `
+Execution tests for the 'fwidthCoarse' builtin function
+
+T is f32 or vecN<f32>
+fn fwidthCoarse(e:T) ->T
+Returns abs(dpdxCoarse(e)) + abs(dpdyCoarse(e)).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthFine.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthFine.spec.ts
new file mode 100644
index 0000000000..b08c293228
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/fwidthFine.spec.ts
@@ -0,0 +1,21 @@
+export const description = `
+Execution tests for the 'fwidthFine' builtin function
+
+T is f32 or vecN<f32>
+fn fwidthFine(e:T) ->T
+Returns abs(dpdxFine(e)) + abs(dpdyFine(e)).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#derivative-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts
new file mode 100644
index 0000000000..1068e76252
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/insertBits.spec.ts
@@ -0,0 +1,386 @@
+export const description = `
+Execution tests for the 'insertBits' builtin function
+
+S is i32 or u32
+T is S or vecN<S>
+@const fn insertBits(e: T, newbits:T, offset: u32, count: u32) -> T Sets bits in an integer.
+
+When T is a scalar type, then:
+ w is the bit width of T
+ o = min(offset,w)
+ c = min(count, w - o)
+
+The result is e if c is 0.
+Otherwise, bits o..o+c-1 of the result are copied from bits 0..c-1 of newbits.
+Other bits of the result are copied from e.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ i32Bits,
+ TypeI32,
+ u32,
+ TypeU32,
+ u32Bits,
+ vec2,
+ vec3,
+ vec4,
+ TypeVec,
+} from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('integer')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`integer tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('signed', [false, true])
+ .combine('width', [1, 2, 3, 4])
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ const scalarType = t.params.signed ? TypeI32 : TypeU32;
+ const T = t.params.width === 1 ? scalarType : TypeVec(t.params.width, scalarType);
+
+ const V = (x: number, y?: number, z?: number, w?: number) => {
+ y = y === undefined ? x : y;
+ z = z === undefined ? x : z;
+ w = w === undefined ? x : w;
+
+ if (t.params.signed) {
+ switch (t.params.width) {
+ case 1:
+ return i32Bits(x);
+ case 2:
+ return vec2(i32Bits(x), i32Bits(y));
+ case 3:
+ return vec3(i32Bits(x), i32Bits(y), i32Bits(z));
+ default:
+ return vec4(i32Bits(x), i32Bits(y), i32Bits(z), i32Bits(w));
+ }
+ } else {
+ switch (t.params.width) {
+ case 1:
+ return u32Bits(x);
+ case 2:
+ return vec2(u32Bits(x), u32Bits(y));
+ case 3:
+ return vec3(u32Bits(x), u32Bits(y), u32Bits(z));
+ default:
+ return vec4(u32Bits(x), u32Bits(y), u32Bits(z), u32Bits(w));
+ }
+ }
+ };
+
+ const all_1 = V(0b11111111111111111111111111111111);
+ const all_0 = V(0b00000000000000000000000000000000);
+ const low_1 = V(0b00000000000000000000000000000001);
+ const low_0 = V(0b11111111111111111111111111111110);
+ const high_1 = V(0b10000000000000000000000000000000);
+ const high_0 = V(0b01111111111111111111111111111111);
+ const pattern = V(
+ 0b10001001010100100010010100100010,
+ 0b11001110001100111000110011100011,
+ 0b10101010101010101010101010101010,
+ 0b01010101010101010101010101010101
+ );
+
+ const cases = [
+ { input: [all_0, all_0, u32(0), u32(32)], expected: all_0 },
+ { input: [all_0, all_0, u32(1), u32(10)], expected: all_0 },
+ { input: [all_0, all_0, u32(2), u32(5)], expected: all_0 },
+ { input: [all_0, all_0, u32(0), u32(1)], expected: all_0 },
+ { input: [all_0, all_0, u32(31), u32(1)], expected: all_0 },
+
+ { input: [all_0, all_1, u32(0), u32(32)], expected: all_1 },
+ { input: [all_1, all_0, u32(0), u32(32)], expected: all_0 },
+ { input: [all_0, all_1, u32(0), u32(1)], expected: low_1 },
+ { input: [all_1, all_0, u32(0), u32(1)], expected: low_0 },
+ { input: [all_0, all_1, u32(31), u32(1)], expected: high_1 },
+ { input: [all_1, all_0, u32(31), u32(1)], expected: high_0 },
+ { input: [all_0, all_1, u32(1), u32(10)], expected: V(0b00000000000000000000011111111110) },
+ { input: [all_1, all_0, u32(1), u32(10)], expected: V(0b11111111111111111111100000000001) },
+ { input: [all_0, all_1, u32(2), u32(5)], expected: V(0b00000000000000000000000001111100) },
+ { input: [all_1, all_0, u32(2), u32(5)], expected: V(0b11111111111111111111111110000011) },
+
+ // Patterns
+ { input: [all_0, pattern, u32(0), u32(32)], expected: pattern },
+ { input: [all_1, pattern, u32(0), u32(32)], expected: pattern },
+ {
+ input: [all_0, pattern, u32(1), u32(31)],
+ expected: V(
+ 0b00010010101001000100101001000100,
+ 0b10011100011001110001100111000110,
+ 0b01010101010101010101010101010100,
+ 0b10101010101010101010101010101010
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(1), u32(31)],
+ expected: V(
+ 0b00010010101001000100101001000101,
+ 0b10011100011001110001100111000111,
+ 0b01010101010101010101010101010101,
+ 0b10101010101010101010101010101011
+ ),
+ },
+ {
+ input: [all_0, pattern, u32(14), u32(18)],
+ expected: V(
+ 0b10001001010010001000000000000000,
+ 0b11100011001110001100000000000000,
+ 0b10101010101010101000000000000000,
+ 0b01010101010101010100000000000000
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(14), u32(18)],
+ expected: V(
+ 0b10001001010010001011111111111111,
+ 0b11100011001110001111111111111111,
+ 0b10101010101010101011111111111111,
+ 0b01010101010101010111111111111111
+ ),
+ },
+ {
+ input: [all_0, pattern, u32(14), u32(7)],
+ expected: V(
+ 0b00000000000010001000000000000000,
+ 0b00000000000110001100000000000000,
+ 0b00000000000010101000000000000000,
+ 0b00000000000101010100000000000000
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(14), u32(7)],
+ expected: V(
+ 0b11111111111010001011111111111111,
+ 0b11111111111110001111111111111111,
+ 0b11111111111010101011111111111111,
+ 0b11111111111101010111111111111111
+ ),
+ },
+ {
+ input: [all_0, pattern, u32(14), u32(4)],
+ expected: V(
+ 0b00000000000000001000000000000000,
+ 0b00000000000000001100000000000000,
+ 0b00000000000000101000000000000000,
+ 0b00000000000000010100000000000000
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(14), u32(4)],
+ expected: V(
+ 0b11111111111111001011111111111111,
+ 0b11111111111111001111111111111111,
+ 0b11111111111111101011111111111111,
+ 0b11111111111111010111111111111111
+ ),
+ },
+ {
+ input: [all_0, pattern, u32(14), u32(3)],
+ expected: V(
+ 0b00000000000000001000000000000000,
+ 0b00000000000000001100000000000000,
+ 0b00000000000000001000000000000000,
+ 0b00000000000000010100000000000000
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(14), u32(3)],
+ expected: V(
+ 0b11111111111111101011111111111111,
+ 0b11111111111111101111111111111111,
+ 0b11111111111111101011111111111111,
+ 0b11111111111111110111111111111111
+ ),
+ },
+ {
+ input: [all_0, pattern, u32(18), u32(3)],
+ expected: V(
+ 0b00000000000010000000000000000000,
+ 0b00000000000011000000000000000000,
+ 0b00000000000010000000000000000000,
+ 0b00000000000101000000000000000000
+ ),
+ },
+ {
+ input: [all_1, pattern, u32(18), u32(3)],
+ expected: V(
+ 0b11111111111010111111111111111111,
+ 0b11111111111011111111111111111111,
+ 0b11111111111010111111111111111111,
+ 0b11111111111101111111111111111111
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(1), u32(31)],
+ expected: V(
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000001,
+ 0b00000000000000000000000000000000,
+ 0b00000000000000000000000000000001
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(1), u32(31)],
+ expected: V(
+ 0b11111111111111111111111111111110,
+ 0b11111111111111111111111111111111,
+ 0b11111111111111111111111111111110,
+ 0b11111111111111111111111111111111
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(14), u32(18)],
+ expected: V(
+ 0b00000000000000000010010100100010,
+ 0b00000000000000000000110011100011,
+ 0b00000000000000000010101010101010,
+ 0b00000000000000000001010101010101
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(14), u32(18)],
+ expected: V(
+ 0b11111111111111111110010100100010,
+ 0b11111111111111111100110011100011,
+ 0b11111111111111111110101010101010,
+ 0b11111111111111111101010101010101
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(14), u32(7)],
+ expected: V(
+ 0b10001001010000000010010100100010,
+ 0b11001110001000000000110011100011,
+ 0b10101010101000000010101010101010,
+ 0b01010101010000000001010101010101
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(14), u32(7)],
+ expected: V(
+ 0b10001001010111111110010100100010,
+ 0b11001110001111111100110011100011,
+ 0b10101010101111111110101010101010,
+ 0b01010101010111111101010101010101
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(14), u32(4)],
+ expected: V(
+ 0b10001001010100000010010100100010,
+ 0b11001110001100000000110011100011,
+ 0b10101010101010000010101010101010,
+ 0b01010101010101000001010101010101
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(14), u32(4)],
+ expected: V(
+ 0b10001001010100111110010100100010,
+ 0b11001110001100111100110011100011,
+ 0b10101010101010111110101010101010,
+ 0b01010101010101111101010101010101
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(14), u32(3)],
+ expected: V(
+ 0b10001001010100100010010100100010,
+ 0b11001110001100100000110011100011,
+ 0b10101010101010100010101010101010,
+ 0b01010101010101000001010101010101
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(14), u32(3)],
+ expected: V(
+ 0b10001001010100111110010100100010,
+ 0b11001110001100111100110011100011,
+ 0b10101010101010111110101010101010,
+ 0b01010101010101011101010101010101
+ ),
+ },
+ {
+ input: [pattern, all_0, u32(18), u32(3)],
+ expected: V(
+ 0b10001001010000100010010100100010,
+ 0b11001110001000111000110011100011,
+ 0b10101010101000101010101010101010,
+ 0b01010101010000010101010101010101
+ ),
+ },
+ {
+ input: [pattern, all_1, u32(18), u32(3)],
+ expected: V(
+ 0b10001001010111100010010100100010,
+ 0b11001110001111111000110011100011,
+ 0b10101010101111101010101010101010,
+ 0b01010101010111010101010101010101
+ ),
+ },
+ {
+ input: [pattern, pattern, u32(18), u32(3)],
+ expected: V(
+ 0b10001001010010100010010100100010,
+ 0b11001110001011111000110011100011,
+ 0b10101010101010101010101010101010,
+ 0b01010101010101010101010101010101
+ ),
+ },
+ {
+ input: [pattern, pattern, u32(14), u32(7)],
+ expected: V(
+ 0b10001001010010001010010100100010,
+ 0b11001110001110001100110011100011,
+ 0b10101010101010101010101010101010,
+ 0b01010101010101010101010101010101
+ ),
+ },
+
+ // Zero count
+ { input: [pattern, all_1, u32(0), u32(0)], expected: pattern },
+ { input: [pattern, all_1, u32(1), u32(0)], expected: pattern },
+ { input: [pattern, all_1, u32(2), u32(0)], expected: pattern },
+ { input: [pattern, all_1, u32(31), u32(0)], expected: pattern },
+ { input: [pattern, all_1, u32(32), u32(0)], expected: pattern },
+ { input: [pattern, all_1, u32(0), u32(0)], expected: pattern },
+ ];
+
+ if (t.params.inputSource !== 'const') {
+ cases.push(
+ ...[
+ // Start overflow
+ { input: [all_0, pattern, u32(50), u32(3)], expected: all_0 },
+ { input: [all_1, pattern, u32(50), u32(3)], expected: all_1 },
+ { input: [pattern, pattern, u32(50), u32(3)], expected: pattern },
+
+ // End overflow
+ { input: [all_0, pattern, u32(0), u32(99)], expected: pattern },
+ { input: [all_1, pattern, u32(0), u32(99)], expected: pattern },
+ { input: [all_0, low_1, u32(31), u32(99)], expected: high_1 },
+ {
+ input: [pattern, pattern, u32(20), u32(99)],
+ expected: V(
+ 0b01010010001000100010010100100010,
+ 0b11001110001100111000110011100011,
+ 0b10101010101010101010101010101010,
+ 0b01010101010101010101010101010101
+ ),
+ },
+ ]
+ );
+ }
+
+ await run(t, builtin('insertBits'), [T, T, TypeU32, TypeU32], T, cfg, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts
new file mode 100644
index 0000000000..ec79aa3807
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/inversesqrt.spec.ts
@@ -0,0 +1,63 @@
+export const description = `
+Execution tests for the 'inverseSqrt' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn inverseSqrt(e: T ) -> T
+Returns the reciprocal of sqrt(e). Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { inverseSqrtInterval } from '../../../../../util/f32_interval.js';
+import { biasedRange, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('inverseSqrt', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // 0 < x <= 1 linearly spread
+ ...linearRange(kValue.f32.positive.min, 1, 100),
+ // 1 <= x < 2^32, biased towards 1
+ ...biasedRange(1, 2 ** 32, 1000),
+ ],
+ 'unfiltered',
+ inverseSqrtInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('inverseSqrt'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts
new file mode 100644
index 0000000000..b443aef4b6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/ldexp.spec.ts
@@ -0,0 +1,95 @@
+export const description = `
+Execution tests for the 'ldexp' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+
+K is AbstractInt, i32
+I is K or vecN<K>, where
+ I is a scalar if T is a scalar, or a vector when T is a vector
+
+@const fn ldexp(e1: T ,e2: I ) -> T
+Returns e1 * 2^e2. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { f32, i32, TypeF32, TypeI32 } from '../../../../../util/conversion.js';
+import { ldexpInterval } from '../../../../../util/f32_interval.js';
+import {
+ biasedRange,
+ fullF32Range,
+ fullI32Range,
+ quantizeToF32,
+ quantizeToI32,
+} from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const makeCase = (e1: number, e2: number): Case => {
+ // Due to the heterogeneous types of the params to ldexp (f32 & i32),
+ // makeBinaryToF32IntervalCase cannot be used here.
+ e1 = quantizeToF32(e1);
+ e2 = quantizeToI32(e2);
+ const expected = ldexpInterval(e1, e2);
+ return { input: [f32(e1), i32(e2)], expected };
+};
+
+export const d = makeCaseCache('ldexp', {
+ f32_non_const: () => {
+ const cases: Array<Case> = [];
+ fullF32Range().forEach(e1 => {
+ fullI32Range().forEach(e2 => {
+ cases.push(makeCase(e1, e2));
+ });
+ });
+ return cases;
+ },
+ f32_const: () => {
+ const cases: Array<Case> = [];
+ fullF32Range().forEach(e1 => {
+ biasedRange(-128, 128, 10).forEach(e2 => {
+ const val = e1 * Math.pow(2, e2);
+ if (val >= kValue.f32.negative.min && val <= kValue.f32.positive.max) {
+ cases.push(makeCase(e1, e2));
+ }
+ });
+ });
+ return cases;
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('ldexp'), [TypeF32, TypeI32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts
new file mode 100644
index 0000000000..1fb54f6fa0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/length.spec.ts
@@ -0,0 +1,107 @@
+export const description = `
+Execution tests for the 'length' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn length(e: T ) -> f32
+Returns the length of e (e.g. abs(e) if T is a scalar, or sqrt(e[0]^2 + e[1]^2 + ...) if T is a vector).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { lengthInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, vectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import {
+ allInputSources,
+ generateUnaryToF32IntervalCases,
+ generateVectorToF32IntervalCases,
+ run,
+} from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('length', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', lengthInterval);
+ },
+ f32_vec2_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(2), 'f32-only', lengthInterval);
+ },
+ f32_vec2_non_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(2), 'unfiltered', lengthInterval);
+ },
+ f32_vec3_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(3), 'f32-only', lengthInterval);
+ },
+ f32_vec3_non_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(3), 'unfiltered', lengthInterval);
+ },
+ f32_vec4_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(4), 'f32-only', lengthInterval);
+ },
+ f32_vec4_non_const: () => {
+ return generateVectorToF32IntervalCases(vectorF32Range(4), 'unfiltered', lengthInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('length'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(t, builtin('length'), [TypeVec(2, TypeF32)], TypeF32, t.params, cases);
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(t, builtin('length'), [TypeVec(3, TypeF32)], TypeF32, t.params, cases);
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(t, builtin('length'), [TypeVec(4, TypeF32)], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts
new file mode 100644
index 0000000000..6d2b16f44e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log.spec.ts
@@ -0,0 +1,71 @@
+export const description = `
+Execution tests for the 'log' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn log(e: T ) -> T
+Returns the natural logarithm of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { logInterval } from '../../../../../util/f32_interval.js';
+import { biasedRange, fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// log's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] }
+const inputs = [
+ ...linearRange(kValue.f32.positive.min, 0.5, 20),
+ ...linearRange(0.5, 2.0, 20),
+ ...biasedRange(2.0, 2 ** 32, 1000),
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('log', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', logInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', logInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('log'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts
new file mode 100644
index 0000000000..4425fb7b1a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/log2.spec.ts
@@ -0,0 +1,71 @@
+export const description = `
+Execution tests for the 'log2' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn log2(e: T ) -> T
+Returns the base-2 logarithm of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { log2Interval } from '../../../../../util/f32_interval.js';
+import { biasedRange, fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// log2's accuracy is defined in three regions { [0, 0.5), [0.5, 2.0], (2.0, +∞] }
+const inputs = [
+ ...linearRange(kValue.f32.positive.min, 0.5, 20),
+ ...linearRange(0.5, 2.0, 20),
+ ...biasedRange(2.0, 2 ** 32, 1000),
+ ...fullF32Range(),
+];
+
+export const d = makeCaseCache('log2', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'f32-only', log2Interval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(inputs, 'unfiltered', log2Interval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('log2'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts
new file mode 100644
index 0000000000..7b8446b176
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/max.spec.ts
@@ -0,0 +1,123 @@
+export const description = `
+Execution tests for the 'max' builtin function
+
+S is AbstractInt, i32, or u32
+T is S or vecN<S>
+@const fn max(e1: T ,e2: T) -> T
+Returns e2 if e1 is less than e2, and e1 otherwise. Component-wise when T is a vector.
+
+S is AbstractFloat, f32, f16
+T is vecN<S>
+@const fn max(e1: T ,e2: T) -> T
+Returns e2 if e1 is less than e2, and e1 otherwise.
+If one operand is a NaN, the other is returned.
+If both operands are NaNs, a NaN is returned.
+Component-wise when T is a vector.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { i32, TypeF32, TypeI32, TypeU32, u32 } from '../../../../../util/conversion.js';
+import { maxInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, generateBinaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+/** Generate set of max test cases from list of interesting values */
+function generateTestCases(
+ values: Array<number>,
+ makeCase: (x: number, y: number) => Case
+): Array<Case> {
+ const cases = new Array<Case>();
+ values.forEach(e => {
+ values.forEach(f => {
+ cases.push(makeCase(e, f));
+ });
+ });
+ return cases;
+}
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('max', {
+ f32: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ maxInterval
+ );
+ },
+});
+
+g.test('abstract_int')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`abstract int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ return { input: [u32(x), u32(y)], expected: u32(Math.max(x, y)) };
+ };
+
+ const test_values: Array<number> = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff];
+ const cases = generateTestCases(test_values, makeCase);
+
+ await run(t, builtin('max'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ return { input: [i32(x), i32(y)], expected: i32(Math.max(x, y)) };
+ };
+
+ const test_values: Array<number> = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000];
+ const cases = generateTestCases(test_values, makeCase);
+
+ await run(t, builtin('max'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('max'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts
new file mode 100644
index 0000000000..3b3e612a66
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/min.spec.ts
@@ -0,0 +1,122 @@
+export const description = `
+Execution tests for the 'min' builtin function
+
+S is AbstractInt, i32, or u32
+T is S or vecN<S>
+@const fn min(e1: T ,e2: T) -> T
+Returns e1 if e1 is less than e2, and e2 otherwise. Component-wise when T is a vector.
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn min(e1: T ,e2: T) -> T
+Returns e2 if e2 is less than e1, and e1 otherwise.
+If one operand is a NaN, the other is returned.
+If both operands are NaNs, a NaN is returned.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { i32, TypeF32, TypeI32, TypeU32, u32 } from '../../../../../util/conversion.js';
+import { minInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, generateBinaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('min', {
+ f32: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ minInterval
+ );
+ },
+});
+
+/** Generate set of min test cases from list of interesting values */
+function generateTestCases(
+ values: Array<number>,
+ makeCase: (x: number, y: number) => Case
+): Array<Case> {
+ const cases = new Array<Case>();
+ values.forEach(e => {
+ values.forEach(f => {
+ cases.push(makeCase(e, f));
+ });
+ });
+ return cases;
+}
+
+g.test('abstract_int')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`abstract int tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ return { input: [u32(x), u32(y)], expected: u32(Math.min(x, y)) };
+ };
+
+ const test_values: Array<number> = [0, 1, 2, 0x70000000, 0x80000000, 0xffffffff];
+ const cases = generateTestCases(test_values, makeCase);
+
+ await run(t, builtin('min'), [TypeU32, TypeU32], TypeU32, t.params, cases);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ return { input: [i32(x), i32(y)], expected: i32(Math.min(x, y)) };
+ };
+
+ const test_values: Array<number> = [-0x70000000, -2, -1, 0, 1, 2, 0x70000000];
+ const cases = generateTestCases(test_values, makeCase);
+
+ await run(t, builtin('min'), [TypeI32, TypeI32], TypeI32, t.params, cases);
+ });
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('min'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts
new file mode 100644
index 0000000000..8f0959ed08
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/mix.spec.ts
@@ -0,0 +1,93 @@
+export const description = `
+Execution tests for the 'mix' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn mix(e1: T, e2: T, e3: T) -> T
+Returns the linear blend of e1 and e2 (e.g. e1*(1-e3)+e2*e3). Component-wise when T is a vector.
+
+T is AbstractFloat, f32, or f16
+T2 is vecN<T>
+@const fn mix(e1: T2, e2: T2, e3: T) -> T2
+Returns the component-wise linear blend of e1 and e2, using scalar blending factor e3 for each component.
+Same as mix(e1,e2,T2(e3)).
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { mixIntervals } from '../../../../../util/f32_interval.js';
+import { sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateTernaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('mix', {
+ f32_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'f32-only',
+ ...mixIntervals
+ );
+ },
+ f32_non_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'unfiltered',
+ ...mixIntervals
+ );
+ },
+});
+
+g.test('matching_abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests with matching params`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('matching_f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 test with matching third param`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('mix'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('matching_f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests with matching third param`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('nonmatching_abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests with vector params and scalar third param`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('nonmatching_f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests with vector params and scalar third param`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('monmatching_f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests with vector params and scalar third param`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts
new file mode 100644
index 0000000000..f82a297b6a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/modf.spec.ts
@@ -0,0 +1,356 @@
+export const description = `
+Execution tests for the 'modf' builtin function
+
+T is f32 or f16
+@const fn modf(e:T) -> result_struct
+Splits |e| into fractional and whole number parts.
+The whole part is (|e| % 1.0), and the fractional part is |e| minus the whole part.
+Returns the result_struct for the given type.
+
+S is f32 or f16
+T is vecN<S>
+@const fn modf(e:T) -> result_struct
+Splits the components of |e| into fractional and whole number parts.
+The |i|'th component of the whole and fractional parts equal the whole and fractional parts of modf(e[i]).
+Returns the result_struct for the given type.
+
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { f32, toVector, TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { modfInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, quantizeToF32, vectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, ExpressionBuilder, run } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+/* @returns an ExpressionBuilder that evaluates modf and returns .whole from the result structure */
+function wholeBuilder(): ExpressionBuilder {
+ return value => `modf(${value}).whole`;
+}
+
+/* @returns an ExpressionBuilder that evaluates modf and returns .fract from the result structure */
+function fractBuilder(): ExpressionBuilder {
+ return value => `modf(${value}).fract`;
+}
+
+/* @returns a fract Case for a given vector input */
+function makeVectorCaseFract(v: number[]): Case {
+ v = v.map(quantizeToF32);
+ const fs = v.map(e => {
+ return modfInterval(e).fract;
+ });
+
+ return { input: toVector(v, f32), expected: fs };
+}
+
+/* @returns a fract Case for a given vector input */
+function makeVectorCaseWhole(v: number[]): Case {
+ v = v.map(quantizeToF32);
+ const ws = v.map(e => {
+ return modfInterval(e).whole;
+ });
+
+ return { input: toVector(v, f32), expected: ws };
+}
+
+export const d = makeCaseCache('modf', {
+ f32_fract: () => {
+ const makeCase = (n: number): Case => {
+ n = quantizeToF32(n);
+ return { input: f32(n), expected: modfInterval(n).fract };
+ };
+ return fullF32Range().map(makeCase);
+ },
+ f32_whole: () => {
+ const makeCase = (n: number): Case => {
+ n = quantizeToF32(n);
+ return { input: f32(n), expected: modfInterval(n).whole };
+ };
+ return fullF32Range().map(makeCase);
+ },
+ f32_vec2_fract: () => {
+ return vectorF32Range(2).map(makeVectorCaseFract);
+ },
+ f32_vec2_whole: () => {
+ return vectorF32Range(2).map(makeVectorCaseWhole);
+ },
+ f32_vec3_fract: () => {
+ return vectorF32Range(3).map(makeVectorCaseFract);
+ },
+ f32_vec3_whole: () => {
+ return vectorF32Range(3).map(makeVectorCaseWhole);
+ },
+ f32_vec4_fract: () => {
+ return vectorF32Range(4).map(makeVectorCaseFract);
+ },
+ f32_vec4_whole: () => {
+ return vectorF32Range(4).map(makeVectorCaseWhole);
+ },
+});
+
+g.test('f32_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is f32
+
+struct __modf_result_f32 {
+ fract : f32, // fractional part
+ whole : f32 // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_fract');
+ await run(t, fractBuilder(), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f32_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is f32
+
+struct __modf_result_f32 {
+ fract : f32, // fractional part
+ whole : f32 // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_whole');
+ await run(t, wholeBuilder(), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f32_vec2_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec2<f32>
+
+struct __modf_result_vec2_f32 {
+ fract : vec2<f32>, // fractional part
+ whole : vec2<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec2_fract');
+ await run(t, fractBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec2_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec2<f32>
+
+struct __modf_result_vec2_f32 {
+ fract : vec2<f32>, // fractional part
+ whole : vec2<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec2_whole');
+ await run(t, wholeBuilder(), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec3_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec3<f32>
+
+struct __modf_result_vec3_f32 {
+ fract : vec3<f32>, // fractional part
+ whole : vec3<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec3_fract');
+ await run(t, fractBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec3_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec3<f32>
+
+struct __modf_result_vec3_f32 {
+ fract : vec3<f32>, // fractional part
+ whole : vec3<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec3_whole');
+ await run(t, wholeBuilder(), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec4_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec4<f32>
+
+struct __modf_result_vec4_f32 {
+ fract : vec4<f32>, // fractional part
+ whole : vec4<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec4_fract');
+ await run(t, fractBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec4_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec4<f32>
+
+struct __modf_result_vec4_f32 {
+ fract : vec4<f32>, // fractional part
+ whole : vec4<f32> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get('f32_vec4_whole');
+ await run(t, wholeBuilder(), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases);
+ });
+
+g.test('f16_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is f16
+
+struct __modf_result_f16 {
+ fract : f16, // fractional part
+ whole : f16 // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is f16
+
+struct __modf_result_f16 {
+ fract : f16, // fractional part
+ whole : f16 // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec2_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec2<f16>
+
+struct __modf_result_vec2_f16 {
+ fract : vec2<f16>, // fractional part
+ whole : vec2<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec2_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec2<f16>
+
+struct __modf_result_vec2_f16 {
+ fract : vec2<f16>, // fractional part
+ whole : vec2<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec3_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec3<f16>
+
+struct __modf_result_vec3_f16 {
+ fract : vec3<f16>, // fractional part
+ whole : vec3<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec3_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec3<f16>
+
+struct __modf_result_vec3_f16 {
+ fract : vec3<f16>, // fractional part
+ whole : vec3<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec4_fract')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec4<f16>
+
+struct __modf_result_vec4_f16 {
+ fract : vec4<f16>, // fractional part
+ whole : vec4<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
+
+g.test('f16_vec4_whole')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+T is vec4<f16>
+
+struct __modf_result_vec4_f16 {
+ fract : vec4<f16>, // fractional part
+ whole : vec4<f16> // whole part
+}
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts
new file mode 100644
index 0000000000..d1900795ad
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/normalize.spec.ts
@@ -0,0 +1,89 @@
+export const description = `
+Execution tests for the 'normalize' builtin function
+
+T is AbstractFloat, f32, or f16
+@const fn normalize(e: vecN<T> ) -> vecN<T>
+Returns a unit vector in the same direction as e.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { normalizeInterval } from '../../../../../util/f32_interval.js';
+import { vectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateVectorToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('normalize', {
+ f32_vec2_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(2), 'f32-only', normalizeInterval);
+ },
+ f32_vec2_non_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(2), 'unfiltered', normalizeInterval);
+ },
+ f32_vec3_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(3), 'f32-only', normalizeInterval);
+ },
+ f32_vec3_non_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(3), 'unfiltered', normalizeInterval);
+ },
+ f32_vec4_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(4), 'f32-only', normalizeInterval);
+ },
+ f32_vec4_non_const: () => {
+ return generateVectorToVectorCases(vectorF32Range(4), 'unfiltered', normalizeInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(t, builtin('normalize'), [TypeVec(2, TypeF32)], TypeVec(2, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(t, builtin('normalize'), [TypeVec(3, TypeF32)], TypeVec(3, TypeF32), t.params, cases);
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(t, builtin('normalize'), [TypeVec(4, TypeF32)], TypeVec(4, TypeF32), t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts
new file mode 100644
index 0000000000..790e54720c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16float.spec.ts
@@ -0,0 +1,88 @@
+export const description = `
+Converts two floating point values to half-precision floating point numbers, and then combines them into one u32 value.
+Component e[i] of the input is converted to a IEEE-754 binary16 value,
+which is then placed in bits 16 × i through 16 × i + 15 of the result.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { anyOf, skipUndefined } from '../../../../../util/compare.js';
+import {
+ f32,
+ pack2x16float,
+ TypeF32,
+ TypeU32,
+ TypeVec,
+ u32,
+ vec2,
+} from '../../../../../util/conversion.js';
+import { cartesianProduct, fullF32Range, quantizeToF32 } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// pack2x16float has somewhat unusual behaviour, specifically around how it is
+// supposed to behave when values go OOB and when they are considered to have
+// gone OOB, so has its own bespoke implementation.
+
+/**
+ * @returns a Case for `pack2x16float`
+ * @param param0 first param for the case
+ * @param param1 second param for the case
+ * @param filter_undefined should inputs that cause an undefined expectation be
+ * filtered out, needed for const-eval
+ */
+function makeCase(param0: number, param1: number, filter_undefined: boolean): Case | undefined {
+ param0 = quantizeToF32(param0);
+ param1 = quantizeToF32(param1);
+
+ const results = pack2x16float(param0, param1);
+ if (filter_undefined && results.some(r => r === undefined)) {
+ return undefined;
+ }
+
+ return {
+ input: [vec2(f32(param0), f32(param1))],
+ expected: anyOf(
+ ...results.map(r => (r === undefined ? skipUndefined(undefined) : skipUndefined(u32(r))))
+ ),
+ };
+}
+
+/**
+ * @returns an array of Cases for `pack2x16float`
+ * @param param0s array of inputs to try for the first param
+ * @param param1s array of inputs to try for the second param
+ * @param filter_undefined should inputs that cause an undefined expectation be
+ * filtered out, needed for const-eval
+ */
+function generateCases(param0s: number[], param1s: number[], filter_undefined: boolean): Case[] {
+ return cartesianProduct(param0s, param1s)
+ .map(e => makeCase(e[0], e[1], filter_undefined))
+ .filter((c): c is Case => c !== undefined);
+}
+
+export const d = makeCaseCache('pack2x16float', {
+ f32_const: () => {
+ return generateCases(fullF32Range(), fullF32Range(), true);
+ },
+ f32_non_const: () => {
+ return generateCases(fullF32Range(), fullF32Range(), false);
+ },
+});
+
+g.test('pack')
+ .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
+ .desc(
+ `
+@const fn pack2x16float(e: vec2<f32>) -> u32
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('pack2x16float'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts
new file mode 100644
index 0000000000..54bb21f6c6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16snorm.spec.ts
@@ -0,0 +1,55 @@
+export const description = `
+Converts two normalized floating point values to 16-bit signed integers, and then combines them into one u32 value.
+Component e[i] of the input is converted to a 16-bit twos complement integer value
+⌊ 0.5 + 32767 × min(1, max(-1, e[i])) ⌋ which is then placed in
+bits 16 × i through 16 × i + 15 of the result.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import {
+ f32,
+ pack2x16snorm,
+ TypeF32,
+ TypeU32,
+ TypeVec,
+ u32,
+ vec2,
+} from '../../../../../util/conversion.js';
+import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('pack')
+ .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
+ .desc(
+ `
+@const fn pack2x16snorm(e: vec2<f32>) -> u32
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ x = quantizeToF32(x);
+ y = quantizeToF32(y);
+ return { input: [vec2(f32(x), f32(y))], expected: u32(pack2x16snorm(x, y)) };
+ };
+
+ // Returns a value normalized to [-1, 1].
+ const normalizeF32 = (n: number): number => {
+ return n / kValue.f32.positive.max;
+ };
+
+ const cases: Array<Case> = vectorF32Range(2).flatMap(v => {
+ return [
+ makeCase(...(v as [number, number])),
+ makeCase(...(v.map(normalizeF32) as [number, number])),
+ ];
+ });
+
+ await run(t, builtin('pack2x16snorm'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts
new file mode 100644
index 0000000000..a875a9c7e1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack2x16unorm.spec.ts
@@ -0,0 +1,55 @@
+export const description = `
+Converts two normalized floating point values to 16-bit unsigned integers, and then combines them into one u32 value.
+Component e[i] of the input is converted to a 16-bit unsigned integer value
+⌊ 0.5 + 65535 × min(1, max(0, e[i])) ⌋ which is then placed in
+bits 16 × i through 16 × i + 15 of the result.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import {
+ f32,
+ pack2x16unorm,
+ TypeF32,
+ TypeU32,
+ TypeVec,
+ u32,
+ vec2,
+} from '../../../../../util/conversion.js';
+import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('pack')
+ .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
+ .desc(
+ `
+@const fn pack2x16unorm(e: vec2<f32>) -> u32
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const makeCase = (x: number, y: number): Case => {
+ x = quantizeToF32(x);
+ y = quantizeToF32(y);
+ return { input: [vec2(f32(x), f32(y))], expected: u32(pack2x16unorm(x, y)) };
+ };
+
+ // Returns a value normalized to [0, 1].
+ const normalizeF32 = (n: number): number => {
+ return n > 0 ? n / kValue.f32.positive.max : n / kValue.f32.negative.min;
+ };
+
+ const cases: Array<Case> = vectorF32Range(2).flatMap(v => {
+ return [
+ makeCase(...(v as [number, number])),
+ makeCase(...(v.map(normalizeF32) as [number, number])),
+ ];
+ });
+
+ await run(t, builtin('pack2x16unorm'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts
new file mode 100644
index 0000000000..de0463e9fc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8snorm.spec.ts
@@ -0,0 +1,60 @@
+export const description = `
+Converts four normalized floating point values to 8-bit signed integers, and then combines them into one u32 value.
+Component e[i] of the input is converted to an 8-bit twos complement integer value
+⌊ 0.5 + 127 × min(1, max(-1, e[i])) ⌋ which is then placed in
+bits 8 × i through 8 × i + 7 of the result.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import {
+ f32,
+ pack4x8snorm,
+ Scalar,
+ TypeF32,
+ TypeU32,
+ TypeVec,
+ u32,
+ vec4,
+} from '../../../../../util/conversion.js';
+import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('pack')
+ .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
+ .desc(
+ `
+@const fn pack4x8snorm(e: vec4<f32>) -> u32
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const makeCase = (vals: [number, number, number, number]): Case => {
+ const vals_f32 = new Array<Scalar>(4) as [Scalar, Scalar, Scalar, Scalar];
+ for (const idx in vals) {
+ vals[idx] = quantizeToF32(vals[idx]);
+ vals_f32[idx] = f32(vals[idx]);
+ }
+
+ return { input: [vec4(...vals_f32)], expected: u32(pack4x8snorm(...vals)) };
+ };
+
+ // Returns a value normalized to [-1, 1].
+ const normalizeF32 = (n: number): number => {
+ return n / kValue.f32.positive.max;
+ };
+
+ const cases: Array<Case> = vectorF32Range(4).flatMap(v => {
+ return [
+ makeCase(v as [number, number, number, number]),
+ makeCase(v.map(normalizeF32) as [number, number, number, number]),
+ ];
+ });
+
+ await run(t, builtin('pack4x8snorm'), [TypeVec(4, TypeF32)], TypeU32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts
new file mode 100644
index 0000000000..b670e92fbb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pack4x8unorm.spec.ts
@@ -0,0 +1,60 @@
+export const description = `
+Converts four normalized floating point values to 8-bit unsigned integers, and then combines them into one u32 value.
+Component e[i] of the input is converted to an 8-bit unsigned integer value
+⌊ 0.5 + 255 × min(1, max(0, e[i])) ⌋ which is then placed in
+bits 8 × i through 8 × i + 7 of the result.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import {
+ f32,
+ pack4x8unorm,
+ Scalar,
+ TypeF32,
+ TypeU32,
+ TypeVec,
+ u32,
+ vec4,
+} from '../../../../../util/conversion.js';
+import { quantizeToF32, vectorF32Range } from '../../../../../util/math.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('pack')
+ .specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
+ .desc(
+ `
+@const fn pack4x8unorm(e: vec4<f32>) -> u32
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const makeCase = (vals: [number, number, number, number]): Case => {
+ const vals_f32 = new Array<Scalar>(4) as [Scalar, Scalar, Scalar, Scalar];
+ for (const idx in vals) {
+ vals[idx] = quantizeToF32(vals[idx]);
+ vals_f32[idx] = f32(vals[idx]);
+ }
+
+ return { input: [vec4(...vals_f32)], expected: u32(pack4x8unorm(...vals)) };
+ };
+
+ // Returns a value normalized to [0, 1].
+ const normalizeF32 = (n: number): number => {
+ return n > 0 ? n / kValue.f32.positive.max : n / kValue.f32.negative.min;
+ };
+
+ const cases: Array<Case> = vectorF32Range(4).flatMap(v => {
+ return [
+ makeCase(v as [number, number, number, number]),
+ makeCase(v.map(normalizeF32) as [number, number, number, number]),
+ ];
+ });
+
+ await run(t, builtin('pack4x8unorm'), [TypeVec(4, TypeF32)], TypeU32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts
new file mode 100644
index 0000000000..8f2b3c9a54
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/pow.spec.ts
@@ -0,0 +1,66 @@
+export const description = `
+Execution tests for the 'pow' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn pow(e1: T ,e2: T ) -> T
+Returns e1 raised to the power e2. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { powInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateBinaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('pow', {
+ f32_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'f32-only',
+ powInterval
+ );
+ },
+ f32_non_const: () => {
+ return generateBinaryToF32IntervalCases(
+ fullF32Range(),
+ fullF32Range(),
+ 'unfiltered',
+ powInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('pow'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts
new file mode 100644
index 0000000000..2c07f640be
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts
@@ -0,0 +1,70 @@
+export const description = `
+Execution tests for the 'quantizeToF16' builtin function
+
+T is f32 or vecN<f32>
+@const fn quantizeToF16(e: T ) -> T
+Quantizes a 32-bit floating point value e as if e were converted to a IEEE 754
+binary16 value, and then converted back to a IEEE 754 binary32 value.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { kValue } from '../../../../../util/constants.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { quantizeToF16Interval } from '../../../../../util/f32_interval.js';
+import { fullF16Range, fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('quantizeToF16', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ kValue.f16.negative.min,
+ kValue.f16.negative.max,
+ kValue.f16.subnormal.negative.min,
+ kValue.f16.subnormal.negative.max,
+ kValue.f16.subnormal.positive.min,
+ kValue.f16.subnormal.positive.max,
+ kValue.f16.positive.min,
+ kValue.f16.positive.max,
+ ...fullF16Range(),
+ ],
+ 'f32-only',
+ quantizeToF16Interval
+ );
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ kValue.f16.negative.min,
+ kValue.f16.negative.max,
+ kValue.f16.subnormal.negative.min,
+ kValue.f16.subnormal.negative.max,
+ kValue.f16.subnormal.positive.min,
+ kValue.f16.subnormal.positive.max,
+ kValue.f16.positive.min,
+ kValue.f16.positive.max,
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ quantizeToF16Interval
+ );
+ },
+});
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('quantizeToF16'), [TypeF32], TypeF32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts
new file mode 100644
index 0000000000..9b53db29b9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/radians.spec.ts
@@ -0,0 +1,54 @@
+export const description = `
+Execution tests for the 'radians' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn radians(e1: T ) -> T
+Converts degrees to radians, approximating e1 * π / 180.
+Component-wise when T is a vector
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { radiansInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('radians', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', radiansInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('radians'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts
new file mode 100644
index 0000000000..a2865e1a85
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reflect.spec.ts
@@ -0,0 +1,137 @@
+export const description = `
+Execution tests for the 'reflect' builtin function
+
+T is vecN<AbstractFloat>, vecN<f32>, or vecN<f16>
+@const fn reflect(e1: T, e2: T ) -> T
+For the incident vector e1 and surface orientation e2, returns the reflection
+direction e1-2*dot(e2,e1)*e2.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeVec } from '../../../../../util/conversion.js';
+import { reflectInterval } from '../../../../../util/f32_interval.js';
+import { sparseVectorF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateVectorPairToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('reflect', {
+ f32_vec2_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'f32-only',
+ reflectInterval
+ );
+ },
+ f32_vec2_non_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ 'unfiltered',
+ reflectInterval
+ );
+ },
+ f32_vec3_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'f32-only',
+ reflectInterval
+ );
+ },
+ f32_vec3_non_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ 'unfiltered',
+ reflectInterval
+ );
+ },
+ f32_vec4_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'f32-only',
+ reflectInterval
+ );
+ },
+ f32_vec4_non_const: () => {
+ return generateVectorPairToVectorCases(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ 'unfiltered',
+ reflectInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(
+ t,
+ builtin('reflect'),
+ [TypeVec(2, TypeF32), TypeVec(2, TypeF32)],
+ TypeVec(2, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(
+ t,
+ builtin('reflect'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32)],
+ TypeVec(3, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(
+ t,
+ builtin('reflect'),
+ [TypeVec(4, TypeF32), TypeVec(4, TypeF32)],
+ TypeVec(4, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts
new file mode 100644
index 0000000000..96a0fbe02a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/refract.spec.ts
@@ -0,0 +1,196 @@
+export const description = `
+Execution tests for the 'refract' builtin function
+
+T is vecN<I>
+I is AbstractFloat, f32, or f16
+@const fn refract(e1: T ,e2: T ,e3: I ) -> T
+For the incident vector e1 and surface normal e2, and the ratio of indices of
+refraction e3, let k = 1.0 -e3*e3* (1.0 - dot(e2,e1) * dot(e2,e1)).
+If k < 0.0, returns the refraction vector 0.0, otherwise return the refraction
+vector e3*e1- (e3* dot(e2,e1) + sqrt(k)) *e2.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { f32, TypeF32, TypeVec, Vector } from '../../../../../util/conversion.js';
+import { refractInterval } from '../../../../../util/f32_interval.js';
+import { sparseVectorF32Range, quantizeToF32, sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, IntervalFilter, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// Using a bespoke implementation of make*Case and generate*Cases here
+// since refract is the only builtin with the API signature
+// (vec, vec, scalar) -> vec
+
+/**
+ * @returns a Case for `refract`
+ * @param i the `i` param for the case
+ * @param s the `s` param for the case
+ * @param r the `r` param for the case
+ * @param check what interval checking to apply
+ * */
+function makeCaseF32(i: number[], s: number[], r: number, check: IntervalFilter): Case | undefined {
+ i = i.map(quantizeToF32);
+ s = s.map(quantizeToF32);
+ r = quantizeToF32(r);
+
+ const i_f32 = i.map(f32);
+ const s_f32 = s.map(f32);
+ const r_f32 = f32(r);
+
+ const vectors = refractInterval(i, s, r);
+ if (check === 'f32-only' && vectors.some(e => !e.isFinite())) {
+ return undefined;
+ }
+
+ return {
+ input: [new Vector(i_f32), new Vector(s_f32), r_f32],
+ expected: refractInterval(i, s, r),
+ };
+}
+
+/**
+ * @returns an array of Cases for `refract`
+ * @param param_is array of inputs to try for the `i` param
+ * @param param_ss array of inputs to try for the `s` param
+ * @param param_rs array of inputs to try for the `r` param
+ * @param check what interval checking to apply
+ */
+function generateCasesF32(
+ param_is: number[][],
+ param_ss: number[][],
+ param_rs: number[],
+ check: IntervalFilter
+): Case[] {
+ // Cannot use `cartesianProduct` here due to heterogeneous param types
+ return param_is
+ .flatMap(i => {
+ return param_ss.flatMap(s => {
+ return param_rs.map(r => {
+ return makeCaseF32(i, s, r, check);
+ });
+ });
+ })
+ .filter((c): c is Case => c !== undefined);
+}
+
+export const d = makeCaseCache('refract', {
+ f32_vec2_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ sparseF32Range(),
+ 'f32-only'
+ );
+ },
+ f32_vec2_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(2),
+ sparseVectorF32Range(2),
+ sparseF32Range(),
+ 'unfiltered'
+ );
+ },
+ f32_vec3_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ sparseF32Range(),
+ 'f32-only'
+ );
+ },
+ f32_vec3_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(3),
+ sparseVectorF32Range(3),
+ sparseF32Range(),
+ 'unfiltered'
+ );
+ },
+ f32_vec4_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ sparseF32Range(),
+ 'f32-only'
+ );
+ },
+ f32_vec4_non_const: () => {
+ return generateCasesF32(
+ sparseVectorF32Range(4),
+ sparseVectorF32Range(4),
+ sparseF32Range(),
+ 'unfiltered'
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
+
+g.test('f32_vec2')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec2s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec2_const' : 'f32_vec2_non_const'
+ );
+ await run(
+ t,
+ builtin('refract'),
+ [TypeVec(2, TypeF32), TypeVec(2, TypeF32), TypeF32],
+ TypeVec(2, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec3')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec3s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec3_const' : 'f32_vec3_non_const'
+ );
+ await run(
+ t,
+ builtin('refract'),
+ [TypeVec(3, TypeF32), TypeVec(3, TypeF32), TypeF32],
+ TypeVec(3, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f32_vec4')
+ .specURL('https://www.w3.org/TR/WGSL/#numeric-builtin-functions')
+ .desc(`f32 tests using vec4s`)
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(
+ t.params.inputSource === 'const' ? 'f32_vec4_const' : 'f32_vec4_non_const'
+ );
+ await run(
+ t,
+ builtin('refract'),
+ [TypeVec(4, TypeF32), TypeVec(4, TypeF32), TypeF32],
+ TypeVec(4, TypeF32),
+ t.params,
+ cases
+ );
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [2, 3, 4] as const))
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts
new file mode 100644
index 0000000000..6acb359822
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/reverseBits.spec.ts
@@ -0,0 +1,250 @@
+export const description = `
+Execution tests for the 'reversBits' builtin function
+
+S is i32, u32
+T is S or vecN<S>
+@const fn reverseBits(e: T ) -> T
+Reverses the bits in e: The bit at position k of the result equals the bit at position 31-k of e.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeU32, u32Bits, TypeI32, i32Bits } from '../../../../../util/conversion.js';
+import { allInputSources, Config, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('u32')
+ .specURL('https://www.w3.org/TR/WGSL/#integer-builtin-functions')
+ .desc(`u32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ // prettier-ignore
+ await run(t, builtin('reverseBits'), [TypeU32], TypeU32, cfg, [
+ // Zero
+ { input: u32Bits(0b00000000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000000000) },
+
+ // One
+ { input: u32Bits(0b00000000000000000000000000000001), expected: u32Bits(0b10000000000000000000000000000000) },
+
+ // 0's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000010), expected: u32Bits(0b01000000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000000100), expected: u32Bits(0b00100000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000001000), expected: u32Bits(0b00010000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000010000), expected: u32Bits(0b00001000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000100000), expected: u32Bits(0b00000100000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000001000000), expected: u32Bits(0b00000010000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000010000000), expected: u32Bits(0b00000001000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000100000000), expected: u32Bits(0b00000000100000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000001000000000), expected: u32Bits(0b00000000010000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000010000000000), expected: u32Bits(0b00000000001000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000100000000000), expected: u32Bits(0b00000000000100000000000000000000) },
+ { input: u32Bits(0b00000000000000000001000000000000), expected: u32Bits(0b00000000000010000000000000000000) },
+ { input: u32Bits(0b00000000000000000010000000000000), expected: u32Bits(0b00000000000001000000000000000000) },
+ { input: u32Bits(0b00000000000000000100000000000000), expected: u32Bits(0b00000000000000100000000000000000) },
+ { input: u32Bits(0b00000000000000001000000000000000), expected: u32Bits(0b00000000000000010000000000000000) },
+ { input: u32Bits(0b00000000000000010000000000000000), expected: u32Bits(0b00000000000000001000000000000000) },
+ { input: u32Bits(0b00000000000000100000000000000000), expected: u32Bits(0b00000000000000000100000000000000) },
+ { input: u32Bits(0b00000000000001000000000000000000), expected: u32Bits(0b00000000000000000010000000000000) },
+ { input: u32Bits(0b00000000000010000000000000000000), expected: u32Bits(0b00000000000000000001000000000000) },
+ { input: u32Bits(0b00000000000100000000000000000000), expected: u32Bits(0b00000000000000000000100000000000) },
+ { input: u32Bits(0b00000000001000000000000000000000), expected: u32Bits(0b00000000000000000000010000000000) },
+ { input: u32Bits(0b00000000010000000000000000000000), expected: u32Bits(0b00000000000000000000001000000000) },
+ { input: u32Bits(0b00000000100000000000000000000000), expected: u32Bits(0b00000000000000000000000100000000) },
+ { input: u32Bits(0b00000001000000000000000000000000), expected: u32Bits(0b00000000000000000000000010000000) },
+ { input: u32Bits(0b00000010000000000000000000000000), expected: u32Bits(0b00000000000000000000000001000000) },
+ { input: u32Bits(0b00000100000000000000000000000000), expected: u32Bits(0b00000000000000000000000000100000) },
+ { input: u32Bits(0b00001000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000010000) },
+ { input: u32Bits(0b00010000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000001000) },
+ { input: u32Bits(0b00100000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000000100) },
+ { input: u32Bits(0b01000000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000000010) },
+ { input: u32Bits(0b10000000000000000000000000000000), expected: u32Bits(0b00000000000000000000000000000001) },
+
+ // 1's after leading 1
+ { input: u32Bits(0b00000000000000000000000000000011), expected: u32Bits(0b11000000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000000111), expected: u32Bits(0b11100000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000001111), expected: u32Bits(0b11110000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000011111), expected: u32Bits(0b11111000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000111111), expected: u32Bits(0b11111100000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000001111111), expected: u32Bits(0b11111110000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32Bits(0b11111111000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000111111111), expected: u32Bits(0b11111111100000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32Bits(0b11111111110000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000011111111111), expected: u32Bits(0b11111111111000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000111111111111), expected: u32Bits(0b11111111111100000000000000000000) },
+ { input: u32Bits(0b00000000000000000001111111111111), expected: u32Bits(0b11111111111110000000000000000000) },
+ { input: u32Bits(0b00000000000000000011111111111111), expected: u32Bits(0b11111111111111000000000000000000) },
+ { input: u32Bits(0b00000000000000000111111111111111), expected: u32Bits(0b11111111111111100000000000000000) },
+ { input: u32Bits(0b00000000000000001111111111111111), expected: u32Bits(0b11111111111111110000000000000000) },
+ { input: u32Bits(0b00000000000000011111111111111111), expected: u32Bits(0b11111111111111111000000000000000) },
+ { input: u32Bits(0b00000000000000111111111111111111), expected: u32Bits(0b11111111111111111100000000000000) },
+ { input: u32Bits(0b00000000000001111111111111111111), expected: u32Bits(0b11111111111111111110000000000000) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32Bits(0b11111111111111111111000000000000) },
+ { input: u32Bits(0b00000000000111111111111111111111), expected: u32Bits(0b11111111111111111111100000000000) },
+ { input: u32Bits(0b00000000001111111111111111111111), expected: u32Bits(0b11111111111111111111110000000000) },
+ { input: u32Bits(0b00000000011111111111111111111111), expected: u32Bits(0b11111111111111111111111000000000) },
+ { input: u32Bits(0b00000000111111111111111111111111), expected: u32Bits(0b11111111111111111111111100000000) },
+ { input: u32Bits(0b00000001111111111111111111111111), expected: u32Bits(0b11111111111111111111111110000000) },
+ { input: u32Bits(0b00000011111111111111111111111111), expected: u32Bits(0b11111111111111111111111111000000) },
+ { input: u32Bits(0b00000111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111100000) },
+ { input: u32Bits(0b00001111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111110000) },
+ { input: u32Bits(0b00011111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111111000) },
+ { input: u32Bits(0b00111111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111111100) },
+ { input: u32Bits(0b01111111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111111110) },
+ { input: u32Bits(0b11111111111111111111111111111111), expected: u32Bits(0b11111111111111111111111111111111) },
+
+ // random after leading 1
+ { input: u32Bits(0b00000000000000000000000000000110), expected: u32Bits(0b01100000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000001101), expected: u32Bits(0b10110000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000011101), expected: u32Bits(0b10111000000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000000111001), expected: u32Bits(0b10011100000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000001101111), expected: u32Bits(0b11110110000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000011111111), expected: u32Bits(0b11111111000000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000000111101111), expected: u32Bits(0b11110111100000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000001111111111), expected: u32Bits(0b11111111110000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000011111110001), expected: u32Bits(0b10001111111000000000000000000000) },
+ { input: u32Bits(0b00000000000000000000111011011101), expected: u32Bits(0b10111011011100000000000000000000) },
+ { input: u32Bits(0b00000000000000000001101101111111), expected: u32Bits(0b11111110110110000000000000000000) },
+ { input: u32Bits(0b00000000000000000011111111011111), expected: u32Bits(0b11111011111111000000000000000000) },
+ { input: u32Bits(0b00000000000000000101111001110101), expected: u32Bits(0b10101110011110100000000000000000) },
+ { input: u32Bits(0b00000000000000001101111011110111), expected: u32Bits(0b11101111011110110000000000000000) },
+ { input: u32Bits(0b00000000000000011111111111110011), expected: u32Bits(0b11001111111111111000000000000000) },
+ { input: u32Bits(0b00000000000000111111111110111111), expected: u32Bits(0b11111101111111111100000000000000) },
+ { input: u32Bits(0b00000000000001111111011111111111), expected: u32Bits(0b11111111111011111110000000000000) },
+ { input: u32Bits(0b00000000000011111111111111111111), expected: u32Bits(0b11111111111111111111000000000000) },
+ { input: u32Bits(0b00000000000111110101011110111111), expected: u32Bits(0b11111101111010101111100000000000) },
+ { input: u32Bits(0b00000000001111101111111111110111), expected: u32Bits(0b11101111111111110111110000000000) },
+ { input: u32Bits(0b00000000011111111111010000101111), expected: u32Bits(0b11110100001011111111111000000000) },
+ { input: u32Bits(0b00000000111111111111001111111011), expected: u32Bits(0b11011111110011111111111100000000) },
+ { input: u32Bits(0b00000001111111011111101111111111), expected: u32Bits(0b11111111110111111011111110000000) },
+ { input: u32Bits(0b00000011101011111011110111111011), expected: u32Bits(0b11011111101111011111010111000000) },
+ { input: u32Bits(0b00000111111110111111111111111111), expected: u32Bits(0b11111111111111111101111111100000) },
+ { input: u32Bits(0b00001111000000011011011110111111), expected: u32Bits(0b11111101111011011000000011110000) },
+ { input: u32Bits(0b00011110101111011111111111111111), expected: u32Bits(0b11111111111111111011110101111000) },
+ { input: u32Bits(0b00110110111111100111111110111101), expected: u32Bits(0b10111101111111100111111101101100) },
+ { input: u32Bits(0b01010111111101111111011111011111), expected: u32Bits(0b11111011111011111110111111101010) },
+ { input: u32Bits(0b11100010011110101101101110101111), expected: u32Bits(0b11110101110110110101111001000111) },
+ ]);
+ });
+
+g.test('i32')
+ .specURL('https://www.w3.org/TR/2021/WD-WGSL-20210929/#integer-builtin-functions')
+ .desc(`i32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cfg: Config = t.params;
+ // prettier-ignore
+ await run(t, builtin('reverseBits'), [TypeI32], TypeI32, cfg, [
+ // Zero
+ { input: i32Bits(0b00000000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000000000) },
+
+ // One
+ { input: i32Bits(0b00000000000000000000000000000001), expected: i32Bits(0b10000000000000000000000000000000) },
+
+ // 0's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000010), expected: i32Bits(0b01000000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000000100), expected: i32Bits(0b00100000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000001000), expected: i32Bits(0b00010000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000010000), expected: i32Bits(0b00001000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000100000), expected: i32Bits(0b00000100000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000001000000), expected: i32Bits(0b00000010000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000010000000), expected: i32Bits(0b00000001000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000100000000), expected: i32Bits(0b00000000100000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000001000000000), expected: i32Bits(0b00000000010000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000010000000000), expected: i32Bits(0b00000000001000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000100000000000), expected: i32Bits(0b00000000000100000000000000000000) },
+ { input: i32Bits(0b00000000000000000001000000000000), expected: i32Bits(0b00000000000010000000000000000000) },
+ { input: i32Bits(0b00000000000000000010000000000000), expected: i32Bits(0b00000000000001000000000000000000) },
+ { input: i32Bits(0b00000000000000000100000000000000), expected: i32Bits(0b00000000000000100000000000000000) },
+ { input: i32Bits(0b00000000000000001000000000000000), expected: i32Bits(0b00000000000000010000000000000000) },
+ { input: i32Bits(0b00000000000000010000000000000000), expected: i32Bits(0b00000000000000001000000000000000) },
+ { input: i32Bits(0b00000000000000100000000000000000), expected: i32Bits(0b00000000000000000100000000000000) },
+ { input: i32Bits(0b00000000000001000000000000000000), expected: i32Bits(0b00000000000000000010000000000000) },
+ { input: i32Bits(0b00000000000010000000000000000000), expected: i32Bits(0b00000000000000000001000000000000) },
+ { input: i32Bits(0b00000000000100000000000000000000), expected: i32Bits(0b00000000000000000000100000000000) },
+ { input: i32Bits(0b00000000001000000000000000000000), expected: i32Bits(0b00000000000000000000010000000000) },
+ { input: i32Bits(0b00000000010000000000000000000000), expected: i32Bits(0b00000000000000000000001000000000) },
+ { input: i32Bits(0b00000000100000000000000000000000), expected: i32Bits(0b00000000000000000000000100000000) },
+ { input: i32Bits(0b00000001000000000000000000000000), expected: i32Bits(0b00000000000000000000000010000000) },
+ { input: i32Bits(0b00000010000000000000000000000000), expected: i32Bits(0b00000000000000000000000001000000) },
+ { input: i32Bits(0b00000100000000000000000000000000), expected: i32Bits(0b00000000000000000000000000100000) },
+ { input: i32Bits(0b00001000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000010000) },
+ { input: i32Bits(0b00010000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000001000) },
+ { input: i32Bits(0b00100000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000000100) },
+ { input: i32Bits(0b01000000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000000010) },
+ { input: i32Bits(0b10000000000000000000000000000000), expected: i32Bits(0b00000000000000000000000000000001) },
+
+ // 1's after leading 1
+ { input: i32Bits(0b00000000000000000000000000000011), expected: i32Bits(0b11000000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000000111), expected: i32Bits(0b11100000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000001111), expected: i32Bits(0b11110000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000011111), expected: i32Bits(0b11111000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000111111), expected: i32Bits(0b11111100000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000001111111), expected: i32Bits(0b11111110000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32Bits(0b11111111000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000111111111), expected: i32Bits(0b11111111100000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32Bits(0b11111111110000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000011111111111), expected: i32Bits(0b11111111111000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000111111111111), expected: i32Bits(0b11111111111100000000000000000000) },
+ { input: i32Bits(0b00000000000000000001111111111111), expected: i32Bits(0b11111111111110000000000000000000) },
+ { input: i32Bits(0b00000000000000000011111111111111), expected: i32Bits(0b11111111111111000000000000000000) },
+ { input: i32Bits(0b00000000000000000111111111111111), expected: i32Bits(0b11111111111111100000000000000000) },
+ { input: i32Bits(0b00000000000000001111111111111111), expected: i32Bits(0b11111111111111110000000000000000) },
+ { input: i32Bits(0b00000000000000011111111111111111), expected: i32Bits(0b11111111111111111000000000000000) },
+ { input: i32Bits(0b00000000000000111111111111111111), expected: i32Bits(0b11111111111111111100000000000000) },
+ { input: i32Bits(0b00000000000001111111111111111111), expected: i32Bits(0b11111111111111111110000000000000) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32Bits(0b11111111111111111111000000000000) },
+ { input: i32Bits(0b00000000000111111111111111111111), expected: i32Bits(0b11111111111111111111100000000000) },
+ { input: i32Bits(0b00000000001111111111111111111111), expected: i32Bits(0b11111111111111111111110000000000) },
+ { input: i32Bits(0b00000000011111111111111111111111), expected: i32Bits(0b11111111111111111111111000000000) },
+ { input: i32Bits(0b00000000111111111111111111111111), expected: i32Bits(0b11111111111111111111111100000000) },
+ { input: i32Bits(0b00000001111111111111111111111111), expected: i32Bits(0b11111111111111111111111110000000) },
+ { input: i32Bits(0b00000011111111111111111111111111), expected: i32Bits(0b11111111111111111111111111000000) },
+ { input: i32Bits(0b00000111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111100000) },
+ { input: i32Bits(0b00001111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111110000) },
+ { input: i32Bits(0b00011111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111111000) },
+ { input: i32Bits(0b00111111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111111100) },
+ { input: i32Bits(0b01111111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111111110) },
+ { input: i32Bits(0b11111111111111111111111111111111), expected: i32Bits(0b11111111111111111111111111111111) },
+
+ // random after leading 1
+ { input: i32Bits(0b00000000000000000000000000000110), expected: i32Bits(0b01100000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000001101), expected: i32Bits(0b10110000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000011101), expected: i32Bits(0b10111000000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000000111001), expected: i32Bits(0b10011100000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000001101111), expected: i32Bits(0b11110110000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000011111111), expected: i32Bits(0b11111111000000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000000111101111), expected: i32Bits(0b11110111100000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000001111111111), expected: i32Bits(0b11111111110000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000011111110001), expected: i32Bits(0b10001111111000000000000000000000) },
+ { input: i32Bits(0b00000000000000000000111011011101), expected: i32Bits(0b10111011011100000000000000000000) },
+ { input: i32Bits(0b00000000000000000001101101111111), expected: i32Bits(0b11111110110110000000000000000000) },
+ { input: i32Bits(0b00000000000000000011111111011111), expected: i32Bits(0b11111011111111000000000000000000) },
+ { input: i32Bits(0b00000000000000000101111001110101), expected: i32Bits(0b10101110011110100000000000000000) },
+ { input: i32Bits(0b00000000000000001101111011110111), expected: i32Bits(0b11101111011110110000000000000000) },
+ { input: i32Bits(0b00000000000000011111111111110011), expected: i32Bits(0b11001111111111111000000000000000) },
+ { input: i32Bits(0b00000000000000111111111110111111), expected: i32Bits(0b11111101111111111100000000000000) },
+ { input: i32Bits(0b00000000000001111111011111111111), expected: i32Bits(0b11111111111011111110000000000000) },
+ { input: i32Bits(0b00000000000011111111111111111111), expected: i32Bits(0b11111111111111111111000000000000) },
+ { input: i32Bits(0b00000000000111110101011110111111), expected: i32Bits(0b11111101111010101111100000000000) },
+ { input: i32Bits(0b00000000001111101111111111110111), expected: i32Bits(0b11101111111111110111110000000000) },
+ { input: i32Bits(0b00000000011111111111010000101111), expected: i32Bits(0b11110100001011111111111000000000) },
+ { input: i32Bits(0b00000000111111111111001111111011), expected: i32Bits(0b11011111110011111111111100000000) },
+ { input: i32Bits(0b00000001111111011111101111111111), expected: i32Bits(0b11111111110111111011111110000000) },
+ { input: i32Bits(0b00000011101011111011110111111011), expected: i32Bits(0b11011111101111011111010111000000) },
+ { input: i32Bits(0b00000111111110111111111111111111), expected: i32Bits(0b11111111111111111101111111100000) },
+ { input: i32Bits(0b00001111000000011011011110111111), expected: i32Bits(0b11111101111011011000000011110000) },
+ { input: i32Bits(0b00011110101111011111111111111111), expected: i32Bits(0b11111111111111111011110101111000) },
+ { input: i32Bits(0b00110110111111100111111110111101), expected: i32Bits(0b10111101111111100111111101101100) },
+ { input: i32Bits(0b01010111111101111111011111011111), expected: i32Bits(0b11111011111011111110111111101010) },
+ { input: i32Bits(0b11100010011110101101101110101111), expected: i32Bits(0b11110101110110110101111001000111) },
+ ]);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts
new file mode 100644
index 0000000000..609ae629ad
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/round.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'round' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn round(e: T) -> T
+Result is the integer k nearest to e, as a floating point value.
+When e lies halfway between integers k and k+1, the result is k when k is even,
+and k+1 when k is odd.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { roundInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('round', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', roundInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('round'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts
new file mode 100644
index 0000000000..d1c83bbdd4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/saturate.spec.ts
@@ -0,0 +1,61 @@
+export const description = `
+Execution tests for the 'saturate' builtin function
+
+S is AbstractFloat, f32, or f16
+T is S or vecN<S>
+@const fn saturate(e: T) -> T
+Returns clamp(e, 0.0, 1.0). Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { saturateInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('saturate', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Non-clamped values
+ ...linearRange(0.0, 1.0, 100),
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ saturateInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('saturate'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts
new file mode 100644
index 0000000000..4e8c4c3799
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/select.spec.ts
@@ -0,0 +1,229 @@
+export const description = `
+Execution tests for the 'select' builtin function
+
+T is scalar, abstract numeric type, or vector
+@const fn select(f: T, t: T, cond: bool) -> T
+Returns t when cond is true, and f otherwise.
+
+T is scalar or abstract numeric type
+@const fn select(f: vecN<T>, t: vecN<T>, cond: vecN<bool>) -> vecN<T>
+Component-wise selection. Result component i is evaluated as select(f[i],t[i],cond[i]).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ VectorType,
+ TypeVec,
+ TypeBool,
+ TypeF32,
+ TypeI32,
+ TypeU32,
+ f32,
+ i32,
+ u32,
+ False,
+ True,
+ bool,
+ vec2,
+ vec3,
+ vec4,
+} from '../../../../../util/conversion.js';
+import { run, CaseList, allInputSources } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+function makeBool(n: number) {
+ return bool((n & 1) === 1);
+}
+
+type scalarKind = 'b' | 'f' | 'i' | 'u';
+
+const dataType = {
+ b: {
+ type: TypeBool,
+ constructor: makeBool,
+ },
+ f: {
+ type: TypeF32,
+ constructor: f32,
+ },
+ i: {
+ type: TypeI32,
+ constructor: i32,
+ },
+ u: {
+ type: TypeU32,
+ constructor: u32,
+ },
+};
+
+g.test('scalar')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-builtin-functions')
+ .desc(`scalar tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('component', ['b', 'f', 'i', 'u'] as const)
+ .combine('overload', ['scalar', 'vec2', 'vec3', 'vec4'] as const)
+ )
+ .fn(async t => {
+ const componentType = dataType[t.params.component as scalarKind].type;
+ const cons = dataType[t.params.component as scalarKind].constructor;
+
+ // Create the scalar values that will be selected from, either as scalars
+ // or vectors.
+ //
+ // Each boolean will select between c[k] and c[k+4]. Those values must
+ // always compare as different. The tricky case is boolean, where the parity
+ // has to be different, i.e. c[k]-c[k+4] must be odd.
+ const c = [0, 1, 2, 3, 5, 6, 7, 8].map(i => cons(i));
+ // Now form vectors that will have different components from each other.
+ const v2a = vec2(c[0], c[1]);
+ const v2b = vec2(c[4], c[5]);
+ const v3a = vec3(c[0], c[1], c[2]);
+ const v3b = vec3(c[4], c[5], c[6]);
+ const v4a = vec4(c[0], c[1], c[2], c[3]);
+ const v4b = vec4(c[4], c[5], c[6], c[7]);
+
+ const overloads = {
+ scalar: {
+ type: componentType,
+ cases: [
+ { input: [c[0], c[1], False], expected: c[0] },
+ { input: [c[0], c[1], True], expected: c[1] },
+ ],
+ },
+ vec2: {
+ type: TypeVec(2, componentType),
+ cases: [
+ { input: [v2a, v2b, False], expected: v2a },
+ { input: [v2a, v2b, True], expected: v2b },
+ ],
+ },
+ vec3: {
+ type: TypeVec(3, componentType),
+ cases: [
+ { input: [v3a, v3b, False], expected: v3a },
+ { input: [v3a, v3b, True], expected: v3b },
+ ],
+ },
+ vec4: {
+ type: TypeVec(4, componentType),
+ cases: [
+ { input: [v4a, v4b, False], expected: v4a },
+ { input: [v4a, v4b, True], expected: v4b },
+ ],
+ },
+ };
+ const overload = overloads[t.params.overload];
+
+ await run(
+ t,
+ builtin('select'),
+ [overload.type, overload.type, TypeBool],
+ overload.type,
+ t.params,
+ overload.cases
+ );
+ });
+
+g.test('vector')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-builtin-functions')
+ .desc(`vector tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('component', ['b', 'f', 'i', 'u'] as const)
+ .combine('overload', ['vec2', 'vec3', 'vec4'] as const)
+ )
+ .fn(async t => {
+ const componentType = dataType[t.params.component as scalarKind].type;
+ const cons = dataType[t.params.component as scalarKind].constructor;
+
+ // Create the scalar values that will be selected from.
+ //
+ // Each boolean will select between c[k] and c[k+4]. Those values must
+ // always compare as different. The tricky case is boolean, where the parity
+ // has to be different, i.e. c[k]-c[k+4] must be odd.
+ const c = [0, 1, 2, 3, 5, 6, 7, 8].map(i => cons(i));
+ const T = True;
+ const F = False;
+
+ let tests: { dataType: VectorType; boolType: VectorType; cases: CaseList };
+
+ switch (t.params.overload) {
+ case 'vec2': {
+ const a = vec2(c[0], c[1]);
+ const b = vec2(c[4], c[5]);
+ tests = {
+ dataType: TypeVec(2, componentType),
+ boolType: TypeVec(2, TypeBool),
+ cases: [
+ { input: [a, b, vec2(F, F)], expected: vec2(a.x, a.y) },
+ { input: [a, b, vec2(F, T)], expected: vec2(a.x, b.y) },
+ { input: [a, b, vec2(T, F)], expected: vec2(b.x, a.y) },
+ { input: [a, b, vec2(T, T)], expected: vec2(b.x, b.y) },
+ ],
+ };
+ break;
+ }
+ case 'vec3': {
+ const a = vec3(c[0], c[1], c[2]);
+ const b = vec3(c[4], c[5], c[6]);
+ tests = {
+ dataType: TypeVec(3, componentType),
+ boolType: TypeVec(3, TypeBool),
+ cases: [
+ { input: [a, b, vec3(F, F, F)], expected: vec3(a.x, a.y, a.z) },
+ { input: [a, b, vec3(F, F, T)], expected: vec3(a.x, a.y, b.z) },
+ { input: [a, b, vec3(F, T, F)], expected: vec3(a.x, b.y, a.z) },
+ { input: [a, b, vec3(F, T, T)], expected: vec3(a.x, b.y, b.z) },
+ { input: [a, b, vec3(T, F, F)], expected: vec3(b.x, a.y, a.z) },
+ { input: [a, b, vec3(T, F, T)], expected: vec3(b.x, a.y, b.z) },
+ { input: [a, b, vec3(T, T, F)], expected: vec3(b.x, b.y, a.z) },
+ { input: [a, b, vec3(T, T, T)], expected: vec3(b.x, b.y, b.z) },
+ ],
+ };
+ break;
+ }
+ case 'vec4': {
+ const a = vec4(c[0], c[1], c[2], c[3]);
+ const b = vec4(c[4], c[5], c[6], c[7]);
+ tests = {
+ dataType: TypeVec(4, componentType),
+ boolType: TypeVec(4, TypeBool),
+ cases: [
+ { input: [a, b, vec4(F, F, F, F)], expected: vec4(a.x, a.y, a.z, a.w) },
+ { input: [a, b, vec4(F, F, F, T)], expected: vec4(a.x, a.y, a.z, b.w) },
+ { input: [a, b, vec4(F, F, T, F)], expected: vec4(a.x, a.y, b.z, a.w) },
+ { input: [a, b, vec4(F, F, T, T)], expected: vec4(a.x, a.y, b.z, b.w) },
+ { input: [a, b, vec4(F, T, F, F)], expected: vec4(a.x, b.y, a.z, a.w) },
+ { input: [a, b, vec4(F, T, F, T)], expected: vec4(a.x, b.y, a.z, b.w) },
+ { input: [a, b, vec4(F, T, T, F)], expected: vec4(a.x, b.y, b.z, a.w) },
+ { input: [a, b, vec4(F, T, T, T)], expected: vec4(a.x, b.y, b.z, b.w) },
+ { input: [a, b, vec4(T, F, F, F)], expected: vec4(b.x, a.y, a.z, a.w) },
+ { input: [a, b, vec4(T, F, F, T)], expected: vec4(b.x, a.y, a.z, b.w) },
+ { input: [a, b, vec4(T, F, T, F)], expected: vec4(b.x, a.y, b.z, a.w) },
+ { input: [a, b, vec4(T, F, T, T)], expected: vec4(b.x, a.y, b.z, b.w) },
+ { input: [a, b, vec4(T, T, F, F)], expected: vec4(b.x, b.y, a.z, a.w) },
+ { input: [a, b, vec4(T, T, F, T)], expected: vec4(b.x, b.y, a.z, b.w) },
+ { input: [a, b, vec4(T, T, T, F)], expected: vec4(b.x, b.y, b.z, a.w) },
+ { input: [a, b, vec4(T, T, T, T)], expected: vec4(b.x, b.y, b.z, b.w) },
+ ],
+ };
+ break;
+ }
+ }
+
+ await run(
+ t,
+ builtin('select'),
+ [tests.dataType, tests.dataType, tests.boolType],
+ tests.dataType,
+ t.params,
+ tests.cases
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts
new file mode 100644
index 0000000000..66f812dd66
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sign.spec.ts
@@ -0,0 +1,53 @@
+export const description = `
+Execution tests for the 'sign' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn sign(e: T ) -> T
+Returns the sign of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { signInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('sign', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', signInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('sign'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts
new file mode 100644
index 0000000000..802977b56f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sin.spec.ts
@@ -0,0 +1,67 @@
+export const description = `
+Execution tests for the 'sin' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn sin(e: T ) -> T
+Returns the sine of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { sinInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('sin', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Well defined accuracy range
+ ...linearRange(-Math.PI, Math.PI, 1000),
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ sinInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(
+ `
+f32 tests
+
+TODO(#792): Decide what the ground-truth is for these tests. [1]
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('sin'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts
new file mode 100644
index 0000000000..8f59ce1e4c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sinh.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'sinh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn sinh(e: T ) -> T
+Returns the hyperbolic sine of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { sinhInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('sinh', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'f32-only', sinhInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', sinhInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('sinh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts
new file mode 100644
index 0000000000..2874f83262
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/smoothstep.spec.ts
@@ -0,0 +1,70 @@
+export const description = `
+Execution tests for the 'smoothstep' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn smoothstep(low: T , high: T , x: T ) -> T
+Returns the smooth Hermite interpolation between 0 and 1.
+Component-wise when T is a vector.
+For scalar T, the result is t * t * (3.0 - 2.0 * t), where t = clamp((x - low) / (high - low), 0.0, 1.0).
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { smoothStepInterval } from '../../../../../util/f32_interval.js';
+import { sparseF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateTernaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('smoothstep', {
+ f32_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'f32-only',
+ smoothStepInterval
+ );
+ },
+ f32_non_const: () => {
+ return generateTernaryToF32IntervalCases(
+ sparseF32Range(),
+ sparseF32Range(),
+ sparseF32Range(),
+ 'unfiltered',
+ smoothStepInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('smoothstep'), [TypeF32, TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts
new file mode 100644
index 0000000000..48980078a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/sqrt.spec.ts
@@ -0,0 +1,56 @@
+export const description = `
+Execution tests for the 'sqrt' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn sqrt(e: T ) -> T
+Returns the square root of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { sqrtInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('sqrt', {
+ f32_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'f32-only', sqrtInterval);
+ },
+ f32_non_const: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', sqrtInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
+ await run(t, builtin('sqrt'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts
new file mode 100644
index 0000000000..24226c9463
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/step.spec.ts
@@ -0,0 +1,85 @@
+export const description = `
+Execution tests for the 'step' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn step(edge: T ,x: T ) -> T
+Returns 1.0 if edge ≤ x, and 0.0 otherwise. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { anyOf } from '../../../../../util/compare.js';
+import { f32, TypeF32 } from '../../../../../util/conversion.js';
+import { stepInterval, toF32Interval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, quantizeToF32 } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, Case, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('step', {
+ f32: () => {
+ const zeroInterval = toF32Interval(0);
+ const oneInterval = toF32Interval(1);
+
+ // stepInterval's return value isn't always interpreted as an acceptance
+ // interval, so makeBinaryToF32IntervalCase cannot be used here.
+ // See the comment block on stepInterval for more details
+ const makeCase = (edge: number, x: number): Case => {
+ edge = quantizeToF32(edge);
+ x = quantizeToF32(x);
+ const expected = stepInterval(edge, x);
+
+ // [0, 0], [1, 1], or [-∞, +∞] cases
+ if (expected.isPoint() || !expected.isFinite()) {
+ return { input: [f32(edge), f32(x)], expected };
+ }
+
+ // [0, 1] case
+ return {
+ input: [f32(edge), f32(x)],
+ expected: anyOf(zeroInterval, oneInterval),
+ };
+ };
+
+ const range = fullF32Range();
+ const cases: Array<Case> = [];
+ range.forEach(edge => {
+ range.forEach(x => {
+ cases.push(makeCase(edge, x));
+ });
+ });
+
+ return cases;
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('step'), [TypeF32, TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/storageBarrier.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/storageBarrier.spec.ts
new file mode 100644
index 0000000000..f376db4472
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/storageBarrier.spec.ts
@@ -0,0 +1,38 @@
+export const description = `
+'storageBarrier' affects memory and atomic operations in the storage address space.
+
+All synchronization functions execute a control barrier with Acquire/Release memory ordering.
+That is, all synchronization functions, and affected memory and atomic operations are ordered
+in program order relative to the synchronization function. Additionally, the affected memory
+and atomic operations program-ordered before the synchronization function must be visible to
+all other threads in the workgroup before any affected memory or atomic operation program-ordered
+after the synchronization function is executed by a member of the workgroup. All synchronization
+functions use the Workgroup memory scope. All synchronization functions have a Workgroup
+execution scope.
+
+All synchronization functions must only be used in the compute shader stage.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions')
+ .desc(
+ `
+All synchronization functions must only be used in the compute shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['vertex', 'fragment', 'compute'] as const))
+ .unimplemented();
+
+g.test('barrier')
+ .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions')
+ .desc(
+ `
+fn storageBarrier()
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts
new file mode 100644
index 0000000000..c226cdd851
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tan.spec.ts
@@ -0,0 +1,61 @@
+export const description = `
+Execution tests for the 'tan' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn tan(e: T ) -> T
+Returns the tangent of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { tanInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range, linearRange } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('tan', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(
+ [
+ // Defined accuracy range
+ ...linearRange(-Math.PI, Math.PI, 100),
+ ...fullF32Range(),
+ ],
+ 'unfiltered',
+ tanInterval
+ );
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('tan'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts
new file mode 100644
index 0000000000..d031331a04
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/tanh.spec.ts
@@ -0,0 +1,53 @@
+export const description = `
+Execution tests for the 'tanh' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn tanh(e: T ) -> T
+Returns the hyperbolic tangent of e. Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { tanhInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('tanh', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', tanhInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('tanh'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts
new file mode 100644
index 0000000000..0ecb9964cf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureDimension.spec.ts
@@ -0,0 +1,160 @@
+export const description = `
+Execution tests for the 'textureDimension' builtin function
+
+The dimensions of the texture in texels.
+For textures based on cubes, the results are the dimensions of each face of the cube.
+Cube faces are square, so the x and y components of the result are equal.
+If level is outside the range [0, textureNumLevels(t)) then any valid value for the return type may be returned.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled')
+ .specURL('https://www.w3.org/TR/WGSL/#texturedimensions')
+ .desc(
+ `
+T: f32, i32, u32
+
+fn textureDimensions(t: texture_1d<T>) -> u32
+fn textureDimensions(t: texture_1d<T>, level: u32) -> u32
+fn textureDimensions(t: texture_2d<T>) -> vec2<u32>
+fn textureDimensions(t: texture_2d<T>, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_2d_array<T>) -> vec2<u32>
+fn textureDimensions(t: texture_2d_array<T>, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_3d<T>) -> vec3<u32>
+fn textureDimensions(t: texture_3d<T>, level: u32) -> vec3<u32>
+fn textureDimensions(t: texture_cube<T>) -> vec2<u32>
+fn textureDimensions(t: texture_cube<T>, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_cube_array<T>) -> vec2<u32>
+fn textureDimensions(t: texture_cube_array<T>, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_multisampled_2d<T>)-> vec2<u32>
+
+Parameters:
+ * t: the sampled texture
+ * level:
+ - The mip level, with level 0 containing a full size version of the texture.
+ - If omitted, the dimensions of level 0 are returned.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', [
+ 'texture_1d',
+ 'texture_2d',
+ 'texture_2d_array',
+ 'texture_3d',
+ 'texture_cube',
+ 'texture_cube_array',
+ 'texture_multisampled_2d',
+ ] as const)
+ .beginSubcases()
+ .combine('sampled_type', ['f32-only', 'i32', 'u32'] as const)
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('depth')
+ .specURL('https://www.w3.org/TR/WGSL/#texturedimensions')
+ .desc(
+ `
+fn textureDimensions(t: texture_depth_2d) -> vec2<u32>
+fn textureDimensions(t: texture_depth_2d, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_depth_2d_array) -> vec2<u32>
+fn textureDimensions(t: texture_depth_2d_array, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_depth_cube) -> vec2<u32>
+fn textureDimensions(t: texture_depth_cube, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_depth_cube_array) -> vec2<u32>
+fn textureDimensions(t: texture_depth_cube_array, level: u32) -> vec2<u32>
+fn textureDimensions(t: texture_depth_multisampled_2d)-> vec2<u32>
+
+Parameters:
+ * t: the depth or multisampled texture
+ * level:
+ - The mip level, with level 0 containing a full size version of the texture.
+ - If omitted, the dimensions of level 0 are returned.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', [
+ 'texture_depth_2d',
+ 'texture_depth_2d_array',
+ 'texture_depth_cube',
+ 'texture_depth_cube_array',
+ 'texture_depth_multisampled_2d',
+ ])
+ .beginSubcases()
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('storage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturedimensions')
+ .desc(
+ `
+F: rgba8unorm
+ rgba8snorm
+ rgba8uint
+ rgba8sint
+ rgba16uint
+ rgba16sint
+ rgba16float
+ r32uint
+ r32sint
+ r32float
+ rg32uint
+ rg32sint
+ rg32float
+ rgba32uint
+ rgba32sint
+ rgba32float
+A: read, write, read_write
+
+fn textureDimensions(t: texture_storage_1d<F,A>) -> u32
+fn textureDimensions(t: texture_storage_2d<F,A>) -> vec2<u32>
+fn textureDimensions(t: texture_storage_2d_array<F,A>) -> vec2<u32>
+fn textureDimensions(t: texture_storage_3d<F,A>) -> vec3<u32>
+
+Parameters:
+ * t: the storage texture
+`
+ )
+ .params(u =>
+ u
+ .combine('texel_format', [
+ 'rgba8unorm',
+ 'rgba8snorm',
+ 'rgba8uint',
+ 'rgba8sint',
+ 'rgba16uint',
+ 'rgba16sint',
+ 'rgba16float',
+ 'r32uint',
+ 'r32sint',
+ 'r32float',
+ 'rg32uint',
+ 'rg32sint',
+ 'rg32float',
+ 'rgba32uint',
+ 'rgba32sint',
+ 'rgba32float',
+ ] as const)
+ .beginSubcases()
+ .combine('access_mode', ['read', 'write', 'read_write'] as const)
+ )
+ .unimplemented();
+
+g.test('external')
+ .specURL('https://www.w3.org/TR/WGSL/#texturedimensions')
+ .desc(
+ `
+fn textureDimensions(t: texture_external) -> vec2<u32>
+
+Parameters:
+ * t: the external texture
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGather.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGather.spec.ts
new file mode 100644
index 0000000000..40b331efab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGather.spec.ts
@@ -0,0 +1,270 @@
+export const description = `
+Execution tests for the 'textureGather' builtin function
+
+A texture gather operation reads from a 2D, 2D array, cube, or cube array texture, computing a four-component vector as follows:
+ * Find the four texels that would be used in a sampling operation with linear filtering, from mip level 0:
+ - Use the specified coordinate, array index (when present), and offset (when present).
+ - The texels are adjacent, forming a square, when considering their texture space coordinates (u,v).
+ - Selected texels at the texture edge, cube face edge, or cube corners are handled as in ordinary texture sampling.
+ * For each texel, read one channel and convert it into a scalar value.
+ - For non-depth textures, a zero-based component parameter specifies the channel to use.
+ * If the texture format supports the specified channel, i.e. has more than component channels:
+ - Yield scalar value v[component] when the texel value is v.
+ * Otherwise:
+ - Yield 0.0 when component is 1 or 2.
+ - Yield 1.0 when component is 3 (the alpha channel).
+ - For depth textures, yield the texel value. (Depth textures only have one channel.)
+ * Yield the four-component vector, arranging scalars produced by the previous step into components according to the relative coordinates of the texels, as follows:
+ - Result component Relative texel coordinate
+ x (umin,vmax)
+ y (umax,vmax)
+ z (umax,vmin)
+ w (umin,vmin)
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+T: i32, u32, f32
+
+fn textureGather(component: C, t: texture_2d<T>, s: sampler, coords: vec2<f32>) -> vec4<T>
+fn textureGather(component: C, t: texture_2d<T>, s: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<T>
+
+Parameters:
+ * component:
+ - The index of the channel to read from the selected texels.
+ - When provided, the component expression must a creation-time expression (e.g. 1).
+ - Its value must be at least 0 and at most 3. Values outside of this range will result in a shader-creation error.
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('T', ['f32-only', 'i32', 'u32'] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+T: i32, u32, f32
+
+fn textureGather(component: C, t: texture_cube<T>, s: sampler, coords: vec3<f32>) -> vec4<T>
+
+Parameters:
+ * component:
+ - The index of the channel to read from the selected texels.
+ - When provided, the component expression must a creation-time expression (e.g. 1).
+ - Its value must be at least 0 and at most 3. Values outside of this range will result in a shader-creation error.
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('T', ['f32-only', 'i32', 'u32'] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ )
+ .unimplemented();
+
+g.test('sampled_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+T: i32, u32, f32
+
+fn textureGather(component: C, t: texture_2d_array<T>, s: sampler, coords: vec2<f32>, array_index: C) -> vec4<T>
+fn textureGather(component: C, t: texture_2d_array<T>, s: sampler, coords: vec2<f32>, array_index: C, offset: vec2<i32>) -> vec4<T>
+
+Parameters:
+ * component:
+ - The index of the channel to read from the selected texels.
+ - When provided, the component expression must a creation-time expression (e.g. 1).
+ - Its value must be at least 0 and at most 3. Values outside of this range will result in a shader-creation error.
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('T', ['f32-only', 'i32', 'u32'] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+T: i32, u32, f32
+
+fn textureGather(component: C, t: texture_cube_array<T>, s: sampler, coords: vec3<f32>, array_index: C) -> vec4<T>
+
+Parameters:
+ * component:
+ - The index of the channel to read from the selected texels.
+ - When provided, the component expression must a creation-time expression (e.g. 1).
+ - Its value must be at least 0 and at most 3. Values outside of this range will result in a shader-creation error.
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index
+`
+ )
+ .paramsSubcasesOnly(
+ u =>
+ u
+ .combine('T', ['f32-only', 'i32', 'u32'] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ )
+ .unimplemented();
+
+g.test('depth_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+fn textureGather(t: texture_depth_2d, s: sampler, coords: vec2<f32>) -> vec4<f32>
+fn textureGather(t: texture_depth_2d, s: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('depth_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+fn textureGather(t: texture_depth_cube, s: sampler, coords: vec3<f32>) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ )
+ .unimplemented();
+
+g.test('depth_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+
+fn textureGather(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C) -> vec4<f32>
+fn textureGather(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('depth_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegather')
+ .desc(
+ `
+C: i32, u32
+
+fn textureGather(t: texture_depth_cube_array, s: sampler, coords: vec3<f32>, array_index: C) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index
+`
+ )
+ .paramsSubcasesOnly(
+ u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGatherCompare.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGatherCompare.spec.ts
new file mode 100644
index 0000000000..c743883ce8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureGatherCompare.spec.ts
@@ -0,0 +1,134 @@
+export const description = `
+Execution tests for the 'textureGatherCompare' builtin function
+
+A texture gather compare operation performs a depth comparison on four texels in a depth texture and collects the results into a single vector, as follows:
+ * Find the four texels that would be used in a depth sampling operation with linear filtering, from mip level 0:
+ - Use the specified coordinate, array index (when present), and offset (when present).
+ - The texels are adjacent, forming a square, when considering their texture space coordinates (u,v).
+ - Selected texels at the texture edge, cube face edge, or cube corners are handled as in ordinary texture sampling.
+ * For each texel, perform a comparison against the depth reference value, yielding a 0.0 or 1.0 value, as controlled by the comparison sampler parameters.
+ * Yield the four-component vector where the components are the comparison results with the texels with relative texel coordinates as follows:
+
+ Result component Relative texel coordinate
+ x (umin,vmax)
+ y (umax,vmax)
+ z (umax,vmin)
+ w (umin,vmin)
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegathercompare')
+ .desc(
+ `
+C: i32, u32
+
+fn textureGatherCompare(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32) -> vec4<f32>
+fn textureGatherCompare(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler_comparison
+ * coords: The texture coordinates
+ * array_index: The 0-based array index.
+ * depth_ref: The reference value to compare the sampled depth value against
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegathercompare')
+ .desc(
+ `
+C: i32, u32
+
+fn textureGatherCompare(t: texture_depth_cube_array, s: sampler_comparison, coords: vec3<f32>, array_index: C, depth_ref: f32) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler_comparison
+ * coords: The texture coordinates
+ * array_index: The 0-based array index.
+ * depth_ref: The reference value to compare the sampled depth value against
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegathercompare')
+ .desc(
+ `
+fn textureGatherCompare(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> vec4<f32>
+fn textureGatherCompare(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler_comparison
+ * coords: The texture coordinates
+ * depth_ref: The reference value to compare the sampled depth value against
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturegathercompare')
+ .desc(
+ `
+fn textureGatherCompare(t: texture_depth_cube, s: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> vec4<f32>
+
+Parameters:
+ * t: The depth texture to read from
+ * s: The sampler_comparison
+ * coords: The texture coordinates
+ * depth_ref: The reference value to compare the sampled depth value against
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts
new file mode 100644
index 0000000000..30cc4fff52
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts
@@ -0,0 +1,185 @@
+export const description = `
+Execution tests for the 'textureLoad' builtin function
+
+Reads a single texel from a texture without sampling or filtering.
+
+Returns the unfiltered texel data.
+
+An out of bounds access occurs if:
+ * any element of coords is outside the range [0, textureDimensions(t, level)) for the corresponding element, or
+ * array_index is outside the range [0, textureNumLayers(t)), or
+ * level is outside the range [0, textureNumLevels(t))
+
+If an out of bounds access occurs, the built-in function returns one of:
+ * The data for some texel within bounds of the texture
+ * A vector (0,0,0,0) or (0,0,0,1) of the appropriate type for non-depth textures
+ * 0.0 for depth textures
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled_1d')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_1d<T>, coords: C, level: C) -> vec4<T>
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * level: The mip level, with level 0 containing a full size version of the texture
+`
+ )
+ .params(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(1))
+ .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_2d')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_2d<T>, coords: vec2<C>, level: C) -> vec4<T>
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * level: The mip level, with level 0 containing a full size version of the texture
+`
+ )
+ .params(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_3d')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_3d<T>, coords: vec3<C>, level: C) -> vec4<T>
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * level: The mip level, with level 0 containing a full size version of the texture
+`
+ )
+ .params(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const)
+ )
+ .unimplemented();
+
+g.test('multisampled')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_multisampled_2d<T>, coords: vec2<C>, sample_index: C)-> vec4<T>
+fn textureLoad(t: texture_depth_multisampled_2d, coords: vec2<C>, sample_index: C)-> f32
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * sample_index: The 0-based sample index of the multisampled texture
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', [
+ 'texture_multisampled_2d',
+ 'texture_depth_multisampled_2d',
+ ] as const)
+ .beginSubcases()
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('sample_index', [-1, 0, `sampleCount-1`, `sampleCount`] as const)
+ )
+ .unimplemented();
+
+g.test('depth')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_depth_2d, coords: vec2<C>, level: C) -> f32
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * level: The mip level, with level 0 containing a full size version of the texture
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const)
+ )
+ .unimplemented();
+
+g.test('external')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_external, coords: vec2<C>) -> vec4<f32>
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u.combine('C', ['i32', 'u32'] as const).combine('coords', generateCoordBoundaries(2))
+ )
+ .unimplemented();
+
+g.test('arrayed')
+ .specURL('https://www.w3.org/TR/WGSL/#textureload')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureLoad(t: texture_2d_array<T>, coords: vec2<C>, array_index: C, level: C) -> vec4<T>
+fn textureLoad(t: texture_depth_2d_array, coords: vec2<C>, array_index: C, level: C) -> f32
+
+Parameters:
+ * t: The sampled texture to read from
+ * coords: The 0-based texel coordinate
+ * array_index: The 0-based texture array index
+ * level: The mip level, with level 0 containing a full size version of the texture
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_2d_array', 'texture_depth_2d_array'] as const)
+ .beginSubcases()
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('array_index', [-1, 0, `numlayers-1`, `numlayers`] as const)
+ .combine('level', [-1, 0, `numlevels-1`, `numlevels`] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts
new file mode 100644
index 0000000000..b845301161
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLayers.spec.ts
@@ -0,0 +1,100 @@
+export const description = `
+Execution tests for the 'textureNumLayers' builtin function
+
+Returns the number of layers (elements) of an array texture.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumlayers')
+ .desc(
+ `
+T, a sampled type.
+
+fn textureNumLayers(t: texture_2d_array<T>) -> u32
+fn textureNumLayers(t: texture_cube_array<T>) -> u32
+
+Parameters
+ * t The sampled array texture.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_2d_array', 'texture_cube_array'] as const)
+ .beginSubcases()
+ .combine('sampled_type', ['f32-only', 'i32', 'u32'] as const)
+ )
+ .unimplemented();
+
+g.test('arrayed')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumlayers')
+ .desc(
+ `
+fn textureNumLayers(t: texture_depth_2d_array) -> u32
+fn textureNumLayers(t: texture_depth_cube_array) -> u32
+
+Parameters
+ * t The depth array texture.
+`
+ )
+ .params(u =>
+ u.combine('texture_type', ['texture_depth_2d_array', 'texture_depth_cube_array'] as const)
+ )
+ .unimplemented();
+
+g.test('storage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumlayers')
+ .desc(
+ `
+F: rgba8unorm
+ rgba8snorm
+ rgba8uint
+ rgba8sint
+ rgba16uint
+ rgba16sint
+ rgba16float
+ r32uint
+ r32sint
+ r32float
+ rg32uint
+ rg32sint
+ rg32float
+ rgba32uint
+ rgba32sint
+ rgba32float
+A: read, write, read_write
+
+fn textureNumLayers(t: texture_storage_2d_array<F,A>) -> u32
+
+Parameters
+ * t The sampled storage array texture.
+`
+ )
+ .params(u =>
+ u
+ .beginSubcases()
+ .combine('texel_format', [
+ 'rgba8unorm',
+ 'rgba8snorm',
+ 'rgba8uint',
+ 'rgba8sint',
+ 'rgba16uint',
+ 'rgba16sint',
+ 'rgba16float',
+ 'r32uint',
+ 'r32sint',
+ 'r32float',
+ 'rg32uint',
+ 'rg32sint',
+ 'rg32float',
+ 'rgba32uint',
+ 'rgba32sint',
+ 'rgba32float',
+ ] as const)
+ .combine('access_mode', ['read', 'write', 'read_write'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLevels.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLevels.spec.ts
new file mode 100644
index 0000000000..4204397b23
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumLevels.spec.ts
@@ -0,0 +1,65 @@
+export const description = `
+Execution tests for the 'textureNumLevels' builtin function
+
+Returns the number of mip levels of a texture.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumlevels')
+ .desc(
+ `
+T, a sampled type.
+
+fn textureNumLevels(t: texture_1d<T>) -> u32
+fn textureNumLevels(t: texture_2d<T>) -> u32
+fn textureNumLevels(t: texture_2d_array<T>) -> u32
+fn textureNumLevels(t: texture_3d<T>) -> u32
+fn textureNumLevels(t: texture_cube<T>) -> u32
+fn textureNumLevels(t: texture_cube_array<T>) -> u32
+
+Parameters
+ * t The sampled array texture.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', [
+ 'texture_1d',
+ 'texture_2d',
+ 'texture_2d_array',
+ 'texture_3d',
+ 'texture_cube',
+ 'texture_cube_array`',
+ ] as const)
+ .beginSubcases()
+ .combine('sampled_type', ['f32-only', 'i32', 'u32'] as const)
+ )
+ .unimplemented();
+
+g.test('depth')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumlevels')
+ .desc(
+ `
+fn textureNumLevels(t: texture_depth_2d) -> u32
+fn textureNumLevels(t: texture_depth_2d_array) -> u32
+fn textureNumLevels(t: texture_depth_cube) -> u32
+fn textureNumLevels(t: texture_depth_cube_array) -> u32
+
+Parameters
+ * t The depth array texture.
+`
+ )
+ .params(u =>
+ u.combine('texture_type', [
+ 'texture_depth_2d',
+ 'texture_depth_2d_array',
+ 'texture_depth_cube',
+ 'texture_depth_cube_array',
+ ] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumSamples.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumSamples.spec.ts
new file mode 100644
index 0000000000..26bda6cd48
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureNumSamples.spec.ts
@@ -0,0 +1,37 @@
+export const description = `
+Execution tests for the 'textureNumSamples' builtin function
+
+Returns the number samples per texel in a multisampled texture.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumsamples')
+ .desc(
+ `
+T, a sampled type.
+
+fn textureNumSamples(t: texture_multisampled_2d<T>) -> u32
+
+Parameters
+ * t The multisampled texture.
+`
+ )
+ .params(u => u.beginSubcases().combine('sampled_type', ['f32-only', 'i32', 'u32'] as const))
+ .unimplemented();
+
+g.test('depth')
+ .specURL('https://www.w3.org/TR/WGSL/#texturenumsamples')
+ .desc(
+ `
+fn textureNumSamples(t: texture_depth_multisampled_2d) -> u32
+
+Parameters
+ * t The multisampled texture.
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts
new file mode 100644
index 0000000000..f5b01dfc63
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts
@@ -0,0 +1,273 @@
+export const description = `
+Samples a texture.
+
+Must only be used in a fragment shader stage.
+Must only be invoked in uniform control flow.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+Tests that 'textureSample' can only be called in 'fragment' shaders.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('control_flow')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+Tests that 'textureSample' can only be called in uniform control flow.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('sampled_1d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+fn textureSample(t: texture_1d<f32>, s: sampler, coords: f32) -> vec4<f32>
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(1))
+ )
+ .unimplemented();
+
+g.test('sampled_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+fn textureSample(t: texture_2d<f32>, s: sampler, coords: vec2<f32>) -> vec4<f32>
+fn textureSample(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+fn textureSample(t: texture_3d<f32>, s: sampler, coords: vec3<f32>) -> vec4<f32>
+fn textureSample(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, offset: vec3<i32>) -> vec4<f32>
+fn textureSample(t: texture_cube<f32>, s: sampler, coords: vec3<f32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_3d', 'texture_cube'] as const)
+ .beginSubcases()
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('offset', generateOffsets(3))
+ )
+ .unimplemented();
+
+g.test('depth_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+fn textureSample(t: texture_depth_2d, s: sampler, coords: vec2<f32>) -> f32
+fn textureSample(t: texture_depth_2d, s: sampler, coords: vec2<f32>, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSample(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C) -> vec4<f32>
+fn textureSample(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSample(t: texture_cube_array<f32>, s: sampler, coords: vec3<f32>, array_index: C) -> vec4<f32>
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+`
+ )
+ .paramsSubcasesOnly(
+ u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ )
+ .unimplemented();
+
+g.test('depth_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+fn textureSample(t: texture_depth_cube, s: sampler, coords: vec3<f32>) -> f32
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ )
+ .unimplemented();
+
+g.test('depth_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSample(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C) -> f32
+fn textureSample(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('depth_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesample')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSample(t: texture_depth_cube_array, s: sampler, coords: vec3<f32>, array_index: C) -> f32
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+`
+ )
+ .paramsSubcasesOnly(
+ u =>
+ u
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts
new file mode 100644
index 0000000000..786bce4830
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleBias.spec.ts
@@ -0,0 +1,163 @@
+export const description = `
+Execution tests for the 'textureSampleBias' builtin function
+
+Samples a texture with a bias to the mip level.
+Must only be used in a fragment shader stage.
+Must only be invoked in uniform control flow.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+Tests that 'textureSampleBias' can only be called in 'fragment' shaders.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('control_flow')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+Tests that 'textureSampleBias' can only be called in uniform control flow.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('sampled_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+fn textureSampleBias(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, bias: f32) -> vec4<f32>
+fn textureSampleBias(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, bias: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * bias: The bias to apply to the mip level before sampling. bias must be between -16.0 and 15.99.
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('bias', [-16.1, -16, 0, 1, 15.99, 16] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+fn textureSampleBias(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32>
+fn textureSampleBias(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, bias: f32, offset: vec3<i32>) -> vec4<f32>
+fn textureSampleBias(t: texture_cube<f32>, s: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32>
+
+Parameters:
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * bias: The bias to apply to the mip level before sampling. bias must be between -16.0 and 15.99.
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_3d', 'texture_cube'] as const)
+ .beginSubcases()
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('bias', [-16.1, -16, 0, 1, 15.99, 16] as const)
+ .combine('offset', generateOffsets(3))
+ )
+ .unimplemented();
+
+g.test('arrayed_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+C: i32, u32
+
+fn textureSampleBias(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, bias: f32) -> vec4<f32>
+fn textureSampleBias(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, bias: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index to sample.
+ * bias: The bias to apply to the mip level before sampling. bias must be between -16.0 and 15.99.
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('bias', [-16.1, -16, 0, 1, 15.99, 16] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('arrayed_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplebias')
+ .desc(
+ `
+C: i32, u32
+
+fn textureSampleBias(t: texture_cube_array<f32>, s: sampler, coords: vec3<f32>, array_index: C, bias: f32) -> vec4<f32>
+
+Parameters:
+ * t: The sampled texture to read from
+ * s: The sampler type
+ * coords: The texture coordinates
+ * array_index: The 0-based texture array index to sample.
+ * bias: The bias to apply to the mip level before sampling. bias must be between -16.0 and 15.99.
+ * offset:
+ - The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ This offset is applied before applying any texture wrapping modes.
+ - The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ - Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('bias', [-16.1, -16, 0, 1, 15.99, 16] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts
new file mode 100644
index 0000000000..9f723fac2e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompare.spec.ts
@@ -0,0 +1,145 @@
+export const description = `
+Samples a depth texture and compares the sampled depth values against a reference value.
+
+Must only be used in a fragment shader stage.
+Must only be invoked in uniform control flow.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+Tests that 'textureSampleCompare' can only be called in 'fragment' shaders.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('control_flow')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+Tests that 'textureSampleCompare' can only be called in uniform control flow.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+fn textureSampleCompare(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32
+fn textureSampleCompare(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * depth_ref The reference value to compare the sampled depth value against.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+fn textureSampleCompare(t: texture_depth_cube, s: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * depth_ref The reference value to compare the sampled depth value against.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
+
+g.test('arrayed_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleCompare(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32) -> f32
+fn textureSampleCompare(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * array_index: The 0-based texture array index to sample.
+ * depth_ref The reference value to compare the sampled depth value against.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('arrayed_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecompare')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleCompare(t: texture_depth_cube_array, s: sampler_comparison, coords: vec3<f32>, array_index: C, depth_ref: f32) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * array_index: The 0-based texture array index to sample.
+ * depth_ref The reference value to compare the sampled depth value against.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompareLevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompareLevel.spec.ts
new file mode 100644
index 0000000000..500df8a6ec
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleCompareLevel.spec.ts
@@ -0,0 +1,149 @@
+export const description = `
+Samples a depth texture and compares the sampled depth values against a reference value.
+
+The textureSampleCompareLevel function is the same as textureSampleCompare, except that:
+
+ * textureSampleCompareLevel always samples texels from mip level 0.
+ * The function does not compute derivatives.
+ * There is no requirement for textureSampleCompareLevel to be invoked in uniform control flow.
+ * textureSampleCompareLevel may be invoked in any shader stage.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+Tests that 'textureSampleCompareLevel' maybe called in any shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('control_flow')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+Tests that 'textureSampleCompareLevel' maybe called in non-uniform control flow.
+`
+ )
+ .params(u => u.combine('stage', ['fragment', 'vertex', 'compute'] as const))
+ .unimplemented();
+
+g.test('2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+fn textureSampleCompareLevel(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(t: texture_depth_2d, s: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * depth_ref The reference value to compare the sampled depth value against.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+fn textureSampleCompareLevel(t: texture_depth_cube, s: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * depth_ref The reference value to compare the sampled depth value against.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
+
+g.test('arrayed_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleCompareLevel(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(t: texture_depth_2d_array, s: sampler_comparison, coords: vec2<f32>, array_index: C, depth_ref: f32, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * array_index: The 0-based texture array index to sample.
+ * depth_ref The reference value to compare the sampled depth value against.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('arrayed_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplecomparelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleCompareLevel(t: texture_depth_cube_array, s: sampler_comparison, coords: vec3<f32>, array_index: C, depth_ref: f32) -> f32
+
+Parameters:
+ * t The depth texture to sample.
+ * s The sampler_comparision type.
+ * coords The texture coordinates used for sampling.
+ * array_index: The 0-based texture array index to sample.
+ * depth_ref The reference value to compare the sampled depth value against.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('depth_ref', [-1 /* smaller ref */, 0 /* equal ref */, 1 /* larger ref */] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleGrad.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleGrad.spec.ts
new file mode 100644
index 0000000000..e0d754ece3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleGrad.spec.ts
@@ -0,0 +1,136 @@
+export const description = `
+Samples a texture using explicit gradients.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplegrad')
+ .desc(
+ `
+fn textureSampleGrad(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32>
+fn textureSampleGrad(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled texture.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * ddx The x direction derivative vector used to compute the sampling locations
+ * ddy The y direction derivative vector used to compute the sampling locations
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplegrad')
+ .desc(
+ `
+fn textureSampleGrad(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+fn textureSampleGrad(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>, offset: vec3<i32>) -> vec4<f32>
+fn textureSampleGrad(t: texture_cube<f32>, s: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled texture.
+ * s The sampler type.
+ * ddx The x direction derivative vector used to compute the sampling locations
+ * ddy The y direction derivative vector used to compute the sampling locations
+ * coords The texture coordinates used for sampling.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('offset', generateOffsets(3))
+ )
+ .unimplemented();
+
+g.test('sampled_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplegrad')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleGrad(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32>
+fn textureSampleGrad(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled texture.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * ddx The x direction derivative vector used to compute the sampling locations
+ * ddy The y direction derivative vector used to compute the sampling locations
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('offset', generateOffsets(2))
+ )
+ .unimplemented();
+
+g.test('sampled_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplegrad')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleGrad(t: texture_cube_array<f32>, s: sampler, coords: vec3<f32>, array_index: C, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled texture.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * ddx The x direction derivative vector used to compute the sampling locations
+ * ddy The y direction derivative vector used to compute the sampling locations
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleLevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleLevel.spec.ts
new file mode 100644
index 0000000000..f8073c65d6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureSampleLevel.spec.ts
@@ -0,0 +1,274 @@
+export const description = `
+Samples a texture.
+
+Must only be used in a fragment shader stage.
+Must only be invoked in uniform control flow.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+import { generateCoordBoundaries, generateOffsets } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sampled_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+fn textureSampleLevel(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, level: f32) -> vec4<f32>
+fn textureSampleLevel(t: texture_2d<f32>, s: sampler, coords: vec2<f32>, level: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleLevel(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, level: f32) -> vec4<f32>
+fn textureSampleLevel(t: texture_2d_array<f32>, s: sampler, coords: vec2<f32>, array_index: C, level: f32, offset: vec2<i32>) -> vec4<f32>
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+fn textureSampleLevel(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, level: f32) -> vec4<f32>
+fn textureSampleLevel(t: texture_3d<f32>, s: sampler, coords: vec3<f32>, level: f32, offset: vec3<i32>) -> vec4<f32>
+fn textureSampleLevel(t: texture_cube<f32>, s: sampler, coords: vec3<f32>, level: f32) -> vec4<f32>
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_3d', 'texture_cube'] as const)
+ .beginSubcases()
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('offset', generateOffsets(3))
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('sampled_array_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleLevel(t: texture_cube_array<f32>, s: sampler, coords: vec3<f32>, array_index: C, level: f32) -> vec4<f32>
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * array_index The 0-based texture array index to sample.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('offset', generateOffsets(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('depth_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleLevel(t: texture_depth_2d, s: sampler, coords: vec2<f32>, level: C) -> f32
+fn textureSampleLevel(t: texture_depth_2d, s: sampler, coords: vec2<f32>, level: C, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('depth_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleLevel(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C, level: C) -> f32
+fn textureSampleLevel(t: texture_depth_2d_array, s: sampler, coords: vec2<f32>, array_index: C, level: C, offset: vec2<i32>) -> f32
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * array_index The 0-based texture array index to sample.
+ * coords The texture coordinates used for sampling.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('offset', generateOffsets(2))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
+
+g.test('depth_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturesamplelevel')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureSampleLevel(t: texture_depth_cube, s: sampler, coords: vec3<f32>, level: C) -> f32
+fn textureSampleLevel(t: texture_depth_cube_array, s: sampler, coords: vec3<f32>, array_index: C, level: C) -> f32
+
+Parameters:
+ * t The sampled or depth texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * level
+ * The mip level, with level 0 containing a full size version of the texture.
+ * For the functions where level is a f32, fractional values may interpolate between
+ two levels if the format is filterable according to the Texture Format Capabilities.
+ * When not specified, mip level 0 is sampled.
+ * offset
+ * The optional texel offset applied to the unnormalized texture coordinate before sampling the texture.
+ * This offset is applied before applying any texture wrapping modes.
+ * The offset expression must be a creation-time expression (e.g. vec2<i32>(1, 2)).
+ * Each offset component must be at least -8 and at most 7.
+ Values outside of this range will result in a shader-creation error.
+`
+ )
+ .params(u =>
+ u
+ .combine('texture_type', ['texture_depth_cube', 'texture_depth_cube_array'] as const)
+ .beginSubcases()
+ .combine('S', ['clamp-to-edge', 'repeat', 'mirror-repeat'])
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ .combine('coords', generateCoordBoundaries(3))
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ .combine('level', [undefined, 0, 1, 'textureNumLevels', 'textureNumLevels+1'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts
new file mode 100644
index 0000000000..efef971e24
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/textureStore.spec.ts
@@ -0,0 +1,122 @@
+export const description = `
+Writes a single texel to a texture.
+
+The channel format T depends on the storage texel format F.
+See the texel format table for the mapping of texel format to channel format.
+
+Note: An out-of-bounds access occurs if:
+ * any element of coords is outside the range [0, textureDimensions(t)) for the corresponding element, or
+ * array_index is outside the range of [0, textureNumLayers(t))
+
+If an out-of-bounds access occurs, the built-in function may do any of the following:
+ * not be executed
+ * store value to some in bounds texel
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TexelFormats } from '../../../../types.js';
+
+import { generateCoordBoundaries } from './utils.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('store_1d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturestore')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureStore(t: texture_storage_1d<F,write>, coords: C, value: vec4<T>)
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * value The new texel value
+`
+ )
+ .params(u =>
+ u
+ .combineWithParams(TexelFormats)
+ .beginSubcases()
+ .combine('coords', generateCoordBoundaries(1))
+ .combine('C', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
+
+g.test('store_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturestore')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureStore(t: texture_storage_2d<F,write>, coords: vec2<C>, value: vec4<T>)
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * value The new texel value
+`
+ )
+ .params(u =>
+ u
+ .combineWithParams(TexelFormats)
+ .beginSubcases()
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('C', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
+
+g.test('store_array_2d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturestore')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureStore(t: texture_storage_2d_array<F,write>, coords: vec2<C>, array_index: C, value: vec4<T>)
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * array_index The 0-based texture array index
+ * coords The texture coordinates used for sampling.
+ * value The new texel value
+`
+ )
+ .params(
+ u =>
+ u
+ .combineWithParams(TexelFormats)
+ .beginSubcases()
+ .combine('coords', generateCoordBoundaries(2))
+ .combine('C', ['i32', 'u32'] as const)
+ .combine('C_value', [-1, 0, 1, 2, 3, 4] as const)
+ /* array_index not param'd as out-of-bounds is implementation specific */
+ )
+ .unimplemented();
+
+g.test('store_3d_coords')
+ .specURL('https://www.w3.org/TR/WGSL/#texturestore')
+ .desc(
+ `
+C is i32 or u32
+
+fn textureStore(t: texture_storage_3d<F,write>, coords: vec3<C>, value: vec4<T>)
+
+Parameters:
+ * t The sampled, depth, or external texture to sample.
+ * s The sampler type.
+ * coords The texture coordinates used for sampling.
+ * value The new texel value
+`
+ )
+ .params(u =>
+ u
+ .combineWithParams(TexelFormats)
+ .beginSubcases()
+ .combine('coords', generateCoordBoundaries(3))
+ .combine('C', ['i32', 'u32'] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts
new file mode 100644
index 0000000000..707b74a38b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/transpose.spec.ts
@@ -0,0 +1,46 @@
+export const description = `
+Execution tests for the 'transpose' builtin function
+
+T is AbstractFloat, f32, or f16
+@const transpose(e: matRxC<T> ) -> matCxR<T>
+Returns the transpose of e.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { allInputSources } from '../../expression.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('rows', [2, 3, 4] as const)
+ .combine('cols', [2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('rows', [2, 3, 4] as const)
+ .combine('cols', [2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#matrix-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u
+ .combine('inputSource', allInputSources)
+ .combine('rows', [2, 3, 4] as const)
+ .combine('cols', [2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts
new file mode 100644
index 0000000000..c743af3230
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/trunc.spec.ts
@@ -0,0 +1,54 @@
+export const description = `
+Execution tests for the 'trunc' builtin function
+
+S is AbstractFloat, f32, f16
+T is S or vecN<S>
+@const fn trunc(e: T ) -> T
+Returns the nearest whole number whose absolute value is less than or equal to e.
+Component-wise when T is a vector.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32 } from '../../../../../util/conversion.js';
+import { truncInterval } from '../../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('trunc', {
+ f32: () => {
+ return generateUnaryToF32IntervalCases(fullF32Range(), 'unfiltered', truncInterval);
+ },
+});
+
+g.test('abstract_float')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`abstract float tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
+
+g.test('f32')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f32 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('f32');
+ await run(t, builtin('trunc'), [TypeF32], TypeF32, t.params, cases);
+ });
+
+g.test('f16')
+ .specURL('https://www.w3.org/TR/WGSL/#float-builtin-functions')
+ .desc(`f16 tests`)
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts
new file mode 100644
index 0000000000..990b957aaf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16float.spec.ts
@@ -0,0 +1,40 @@
+export const description = `
+Decomposes a 32-bit value into two 16-bit chunks, and reinterpets each chunk as
+a floating point value.
+Component i of the result is the f32 representation of v, where v is the
+interpretation of bits 16×i through 16×i+15 of e as an IEEE-754 binary16 value.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js';
+import { unpack2x16floatInterval } from '../../../../../util/f32_interval.js';
+import { fullU32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateU32ToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unpack2x16float', {
+ u32_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'f32-only', unpack2x16floatInterval);
+ },
+ u32_non_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'unfiltered', unpack2x16floatInterval);
+ },
+});
+
+g.test('unpack')
+ .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions')
+ .desc(
+ `
+@const fn unpack2x16float(e: u32) -> vec2<f32>
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const');
+ await run(t, builtin('unpack2x16float'), [TypeU32], TypeVec(2, TypeF32), t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts
new file mode 100644
index 0000000000..6b003ad5d2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16snorm.spec.ts
@@ -0,0 +1,40 @@
+export const description = `
+Decomposes a 32-bit value into two 16-bit chunks, then reinterprets each chunk
+as a signed normalized floating point value.
+Component i of the result is max(v ÷ 32767, -1), where v is the interpretation
+of bits 16×i through 16×i+15 of e as a twos-complement signed integer.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js';
+import { unpack2x16snormInterval } from '../../../../../util/f32_interval.js';
+import { fullU32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateU32ToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unpack2x16snorm', {
+ u32_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'f32-only', unpack2x16snormInterval);
+ },
+ u32_non_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'unfiltered', unpack2x16snormInterval);
+ },
+});
+
+g.test('unpack')
+ .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions')
+ .desc(
+ `
+@const fn unpack2x16snorm(e: u32) -> vec2<f32>
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const');
+ await run(t, builtin('unpack2x16snorm'), [TypeU32], TypeVec(2, TypeF32), t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts
new file mode 100644
index 0000000000..f79047668e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack2x16unorm.spec.ts
@@ -0,0 +1,40 @@
+export const description = `
+Decomposes a 32-bit value into two 16-bit chunks, then reinterprets each chunk
+as an unsigned normalized floating point value.
+Component i of the result is v ÷ 65535, where v is the interpretation of bits
+16×i through 16×i+15 of e as an unsigned integer.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js';
+import { unpack2x16unormInterval } from '../../../../../util/f32_interval.js';
+import { fullU32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateU32ToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unpack2x16unorm', {
+ u32_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'f32-only', unpack2x16unormInterval);
+ },
+ u32_non_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'unfiltered', unpack2x16unormInterval);
+ },
+});
+
+g.test('unpack')
+ .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions')
+ .desc(
+ `
+@const fn unpack2x16unorm(e: u32) -> vec2<f32>
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const');
+ await run(t, builtin('unpack2x16unorm'), [TypeU32], TypeVec(2, TypeF32), t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts
new file mode 100644
index 0000000000..8f425a46f4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8snorm.spec.ts
@@ -0,0 +1,40 @@
+export const description = `
+Decomposes a 32-bit value into four 8-bit chunks, then reinterprets each chunk
+as a signed normalized floating point value.
+Component i of the result is max(v ÷ 127, -1), where v is the interpretation of
+bits 8×i through 8×i+7 of e as a twos-complement signed integer.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js';
+import { unpack4x8snormInterval } from '../../../../../util/f32_interval.js';
+import { fullU32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateU32ToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unpack4x8snorm', {
+ u32_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'f32-only', unpack4x8snormInterval);
+ },
+ u32_non_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'unfiltered', unpack4x8snormInterval);
+ },
+});
+
+g.test('unpack')
+ .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions')
+ .desc(
+ `
+@const fn unpack4x8snorm(e: u32) -> vec4<f32>
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const');
+ await run(t, builtin('unpack4x8snorm'), [TypeU32], TypeVec(4, TypeF32), t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts
new file mode 100644
index 0000000000..01d3db47c9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/unpack4x8unorm.spec.ts
@@ -0,0 +1,40 @@
+export const description = `
+Decomposes a 32-bit value into four 8-bit chunks, then reinterprets each chunk
+as an unsigned normalized floating point value.
+Component i of the result is v ÷ 255, where v is the interpretation of bits 8×i
+through 8×i+7 of e as an unsigned integer.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import { TypeF32, TypeU32, TypeVec } from '../../../../../util/conversion.js';
+import { unpack4x8unormInterval } from '../../../../../util/f32_interval.js';
+import { fullU32Range } from '../../../../../util/math.js';
+import { makeCaseCache } from '../../case_cache.js';
+import { allInputSources, generateU32ToVectorCases, run } from '../../expression.js';
+
+import { builtin } from './builtin.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unpack4x8unorm', {
+ u32_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'f32-only', unpack4x8unormInterval);
+ },
+ u32_non_const: () => {
+ return generateU32ToVectorCases(fullU32Range(), 'unfiltered', unpack4x8unormInterval);
+ },
+});
+
+g.test('unpack')
+ .specURL('https://www.w3.org/TR/WGSL/#unpack-builtin-functions')
+ .desc(
+ `
+@const fn unpack4x8unorm(e: u32) -> vec4<f32>
+`
+ )
+ .params(u => u.combine('inputSource', allInputSources))
+ .fn(async t => {
+ const cases = await d.get(t.params.inputSource === 'const' ? 'u32_const' : 'u32_non_const');
+ await run(t, builtin('unpack4x8unorm'), [TypeU32], TypeVec(4, TypeF32), t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/utils.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/utils.ts
new file mode 100644
index 0000000000..9cbee00939
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/utils.ts
@@ -0,0 +1,45 @@
+/**
+ * Generates the boundary entries for the given number of dimensions
+ *
+ * @param numDimensions: The number of dimensions to generate for
+ * @returns an array of generated coord boundaries
+ */
+export function generateCoordBoundaries(numDimensions: number) {
+ const ret = ['in-bounds'];
+
+ if (numDimensions < 1 || numDimensions > 3) {
+ throw new Error(`invalid numDimensions: ${numDimensions}`);
+ }
+
+ const name = 'xyz';
+ for (let i = 0; i < numDimensions; ++i) {
+ for (const j of ['min', 'max']) {
+ for (const k of ['wrap', 'boundary']) {
+ ret.push(`${name[i]}-${j}-${k}`);
+ }
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * Generates a set of offset values to attempt in the range [-9, 8].
+ *
+ * @param numDimensions: The number of dimensions to generate for
+ * @return an array of generated offset values
+ */
+export function generateOffsets(numDimensions: number) {
+ if (numDimensions < 2 || numDimensions > 3) {
+ throw new Error(`generateOffsets: invalid numDimensions: ${numDimensions}`);
+ }
+ const ret: Array<undefined | Array<number>> = [undefined];
+ for (const val of [-9, -8, 0, 1, 7, 8]) {
+ const v = [];
+ for (let i = 0; i < numDimensions; ++i) {
+ v.push(val);
+ }
+ ret.push(v);
+ }
+ return ret;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupBarrier.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupBarrier.spec.ts
new file mode 100644
index 0000000000..74e0f12325
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/call/builtin/workgroupBarrier.spec.ts
@@ -0,0 +1,38 @@
+export const description = `
+'workgroupBarrier' affects memory and atomic operations in the workgroup address space.
+
+All synchronization functions execute a control barrier with Acquire/Release memory ordering.
+That is, all synchronization functions, and affected memory and atomic operations are ordered
+in program order relative to the synchronization function. Additionally, the affected memory
+and atomic operations program-ordered before the synchronization function must be visible to
+all other threads in the workgroup before any affected memory or atomic operation program-ordered
+after the synchronization function is executed by a member of the workgroup. All synchronization
+functions use the Workgroup memory scope. All synchronization functions have a Workgroup
+execution scope.
+
+All synchronization functions must only be used in the compute shader stage.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('stage')
+ .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions')
+ .desc(
+ `
+All synchronization functions must only be used in the compute shader stage.
+`
+ )
+ .params(u => u.combine('stage', ['vertex', 'fragment', 'compute'] as const))
+ .unimplemented();
+
+g.test('barrier')
+ .specURL('https://www.w3.org/TR/WGSL/#sync-builtin-functions')
+ .desc(
+ `
+fn workgroupBarrier()
+`
+ )
+ .unimplemented();
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts
new file mode 100644
index 0000000000..6bd4dc5b7e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/case_cache.ts
@@ -0,0 +1,209 @@
+import { Cacheable, dataCache } from '../../../../common/framework/data_cache.js';
+import { SerializedComparator, deserializeComparator } from '../../../util/compare.js';
+import {
+ Scalar,
+ Vector,
+ serializeValue,
+ SerializedValue,
+ deserializeValue,
+} from '../../../util/conversion.js';
+import {
+ deserializeF32Interval,
+ F32Interval,
+ SerializedF32Interval,
+ serializeF32Interval,
+} from '../../../util/f32_interval.js';
+
+import { Case, CaseList, Expectation } from './expression.js';
+
+/**
+ * SerializedExpectationValue holds the serialized form of an Expectation when
+ * the Expectation is a Value
+ * This form can be safely encoded to JSON.
+ */
+type SerializedExpectationValue = {
+ kind: 'value';
+ value: SerializedValue;
+};
+
+/**
+ * SerializedExpectationValue holds the serialized form of an Expectation when
+ * the Expectation is an Interval
+ * This form can be safely encoded to JSON.
+ */
+type SerializedExpectationInterval = {
+ kind: 'interval';
+ value: SerializedF32Interval;
+};
+
+/**
+ * SerializedExpectationValue holds the serialized form of an Expectation when
+ * the Expectation is a list of Intervals
+ * This form can be safely encoded to JSON.
+ */
+type SerializedExpectationIntervals = {
+ kind: 'intervals';
+ value: SerializedF32Interval[];
+};
+
+/**
+ * SerializedExpectationValue holds the serialized form of an Expectation when
+ * the Expectation is a Comparator
+ * This form can be safely encoded to JSON.
+ */
+type SerializedExpectationComparator = {
+ kind: 'comparator';
+ value: SerializedComparator;
+};
+
+/**
+ * SerializedExpectation holds the serialized form of an Expectation.
+ * This form can be safely encoded to JSON.
+ */
+export type SerializedExpectation =
+ | SerializedExpectationValue
+ | SerializedExpectationInterval
+ | SerializedExpectationIntervals
+ | SerializedExpectationComparator;
+
+/** serializeExpectation() converts an Expectation to a SerializedExpectation */
+export function serializeExpectation(e: Expectation): SerializedExpectation {
+ if (e instanceof Scalar || e instanceof Vector) {
+ return { kind: 'value', value: serializeValue(e) };
+ }
+ if (e instanceof F32Interval) {
+ return { kind: 'interval', value: serializeF32Interval(e) };
+ }
+ if (e instanceof Array) {
+ return { kind: 'intervals', value: e.map(i => serializeF32Interval(i)) };
+ }
+ if (e instanceof Function) {
+ const comp = (e as unknown) as SerializedComparator;
+ if (comp !== undefined) {
+ // if blocks used to refine the type of comp.kind, otherwise it is
+ // actually the union of the string values
+ if (comp.kind === 'anyOf') {
+ return { kind: 'comparator', value: { kind: comp.kind, data: comp.data } };
+ }
+ if (comp.kind === 'skipUndefined') {
+ return { kind: 'comparator', value: { kind: comp.kind, data: comp.data } };
+ }
+ }
+ throw 'cannot serialize comparator';
+ }
+ throw 'cannot serialize expectation';
+}
+
+/** deserializeExpectation() converts a SerializedExpectation to a Expectation */
+export function deserializeExpectation(data: SerializedExpectation): Expectation {
+ switch (data.kind) {
+ case 'value':
+ return deserializeValue(data.value);
+ case 'interval':
+ return deserializeF32Interval(data.value);
+ case 'intervals':
+ return data.value.map(i => deserializeF32Interval(i));
+ case 'comparator':
+ return deserializeComparator(data.value);
+ }
+}
+
+/**
+ * SerializedCase holds the serialized form of a Case.
+ * This form can be safely encoded to JSON.
+ */
+export type SerializedCase = {
+ input: SerializedValue | SerializedValue[];
+ expected: SerializedExpectation;
+};
+
+/** serializeCase() converts an Case to a SerializedCase */
+export function serializeCase(c: Case): SerializedCase {
+ return {
+ input: c.input instanceof Array ? c.input.map(v => serializeValue(v)) : serializeValue(c.input),
+ expected: serializeExpectation(c.expected),
+ };
+}
+
+/** serializeCase() converts an SerializedCase to a Case */
+export function deserializeCase(data: SerializedCase): Case {
+ return {
+ input:
+ data.input instanceof Array
+ ? data.input.map(v => deserializeValue(v))
+ : deserializeValue(data.input),
+ expected: deserializeExpectation(data.expected),
+ };
+}
+
+/** CaseListBuilder is a function that builds a CaseList */
+export type CaseListBuilder = () => CaseList;
+
+/**
+ * CaseCache is a cache of CaseList.
+ * CaseCache implements the Cacheable interface, so the cases can be pre-built
+ * and stored in the data cache, reducing computation costs at CTS runtime.
+ */
+export class CaseCache implements Cacheable<Record<string, CaseList>> {
+ /**
+ * Constructor
+ * @param name the name of the cache. This must be globally unique.
+ * @param builders a Record of case-list name to case-list builder.
+ */
+ constructor(name: string, builders: Record<string, CaseListBuilder>) {
+ this.path = `webgpu/shader/execution/case-cache/${name}.json`;
+ this.builders = builders;
+ }
+
+ /** get() returns the list of cases with the given name */
+ public async get(name: string): Promise<CaseList> {
+ const data = await dataCache.fetch(this);
+ return data[name];
+ }
+
+ /**
+ * build() implements the Cacheable.build interface.
+ * @returns the data.
+ */
+ build(): Promise<Record<string, CaseList>> {
+ const built: Record<string, CaseList> = {};
+ for (const name in this.builders) {
+ const cases = this.builders[name]();
+ built[name] = cases;
+ }
+ return Promise.resolve(built);
+ }
+
+ /**
+ * serialize() implements the Cacheable.serialize interface.
+ * @returns the serialized data.
+ */
+ serialize(data: Record<string, CaseList>): string {
+ const serialized: Record<string, SerializedCase[]> = {};
+ for (const name in data) {
+ serialized[name] = data[name].map(c => serializeCase(c));
+ }
+ return JSON.stringify(serialized);
+ }
+
+ /**
+ * deserialize() implements the Cacheable.deserialize interface.
+ * @returns the deserialize data.
+ */
+ deserialize(serialized: string): Record<string, CaseList> {
+ const data = JSON.parse(serialized) as Record<string, SerializedCase[]>;
+ const casesByName: Record<string, CaseList> = {};
+ for (const name in data) {
+ const cases = data[name].map(caseData => deserializeCase(caseData));
+ casesByName[name] = cases;
+ }
+ return casesByName;
+ }
+
+ public readonly path: string;
+ private readonly builders: Record<string, CaseListBuilder>;
+}
+
+export function makeCaseCache(name: string, builders: Record<string, CaseListBuilder>): CaseCache {
+ return new CaseCache(name, builders);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts
new file mode 100644
index 0000000000..46f06a2c07
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/expression.ts
@@ -0,0 +1,1137 @@
+import { globalTestConfig } from '../../../../common/framework/test_config.js';
+import { assert } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { compare, Comparator, anyOf } from '../../../util/compare.js';
+import {
+ ScalarType,
+ Scalar,
+ Type,
+ TypeVec,
+ TypeU32,
+ Value,
+ Vector,
+ VectorType,
+ f32,
+ u32,
+ i32,
+} from '../../../util/conversion.js';
+import {
+ BinaryToInterval,
+ F32Interval,
+ PointToInterval,
+ PointToVector,
+ TernaryToInterval,
+ VectorPairToInterval,
+ VectorPairToVector,
+ VectorToInterval,
+ VectorToVector,
+} from '../../../util/f32_interval.js';
+import { cartesianProduct, quantizeToF32, quantizeToU32 } from '../../../util/math.js';
+
+export type Expectation = Value | F32Interval | F32Interval[] | Comparator;
+
+/** Is this expectation actually a Comparator */
+function isComparator(e: Expectation): boolean {
+ return !(
+ e instanceof F32Interval ||
+ e instanceof Scalar ||
+ e instanceof Vector ||
+ e instanceof Array
+ );
+}
+
+/** Helper for converting Values to Comparators */
+export function toComparator(input: Expectation): Comparator {
+ if (!isComparator(input)) {
+ return got => compare(got, input as Value);
+ }
+ return input as Comparator;
+}
+
+/** Case is a single expression test case. */
+export type Case = {
+ // The input value(s)
+ input: Value | Array<Value>;
+ // The expected result, or function to check the result
+ expected: Expectation;
+};
+
+/** CaseList is a list of Cases */
+export type CaseList = Array<Case>;
+
+/** The input value source */
+export type InputSource =
+ | 'const' // Shader creation time constant values (@const)
+ | 'uniform' // Uniform buffer
+ | 'storage_r' // Read-only storage buffer
+ | 'storage_rw'; // Read-write storage buffer
+
+/** All possible input sources */
+export const allInputSources: InputSource[] = ['const', 'uniform', 'storage_r', 'storage_rw'];
+
+/** Configuration for running a expression test */
+export type Config = {
+ // Where the input values are read from
+ inputSource: InputSource;
+ // If defined, scalar test cases will be packed into vectors of the given
+ // width, which must be 2, 3 or 4.
+ // Requires that all parameters of the expression overload are of a scalar
+ // type, and the return type of the expression overload is also a scalar type.
+ // If the number of test cases is not a multiple of the vector width, then the
+ // last scalar value is repeated to fill the last vector value.
+ vectorize?: number;
+};
+
+// Helper for returning the WGSL storage type for the given Type.
+function storageType(ty: Type): Type {
+ if (ty instanceof ScalarType) {
+ if (ty.kind === 'bool') {
+ return TypeU32;
+ }
+ }
+ if (ty instanceof VectorType) {
+ return TypeVec(ty.width, storageType(ty.elementType) as ScalarType);
+ }
+ return ty;
+}
+
+// Helper for converting a value of the type 'ty' from the storage type.
+function fromStorage(ty: Type, expr: string): string {
+ if (ty instanceof ScalarType) {
+ if (ty.kind === 'bool') {
+ return `${expr} != 0u`;
+ }
+ }
+ if (ty instanceof VectorType) {
+ if (ty.elementType.kind === 'bool') {
+ return `${expr} != vec${ty.width}<u32>(0u)`;
+ }
+ }
+ return expr;
+}
+
+// Helper for converting a value of the type 'ty' to the storage type.
+function toStorage(ty: Type, expr: string): string {
+ if (ty instanceof ScalarType) {
+ if (ty.kind === 'bool') {
+ return `select(0u, 1u, ${expr})`;
+ }
+ }
+ if (ty instanceof VectorType) {
+ if (ty.elementType.kind === 'bool') {
+ return `select(vec${ty.width}<u32>(0u), vec${ty.width}<u32>(1u), ${expr})`;
+ }
+ }
+ return expr;
+}
+
+// Currently all values are packed into buffers of 16 byte strides
+const kValueStride = 16;
+
+// ExpressionBuilder returns the WGSL used to test an expression.
+export interface ExpressionBuilder {
+ (values: Array<string>): string;
+}
+
+// A Pipeline is a map of WGSL shader source to a built pipeline
+type PipelineCache = Map<String, GPUComputePipeline>;
+
+/**
+ * Searches for an entry with the given key, adding and returning the result of calling
+ * @p create if the entry was not found.
+ * @param map the cache map
+ * @param key the entry's key
+ * @param create the function used to construct a value, if not found in the cache
+ * @returns the value, either fetched from the cache, or newly built.
+ */
+function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V) {
+ const existing = map.get(key);
+ if (existing !== undefined) {
+ return existing;
+ }
+ const value = create();
+ map.set(key, value);
+ return value;
+}
+/**
+ * Runs the list of expression tests, possibly splitting the tests into multiple
+ * dispatches to keep the input data within the buffer binding limits.
+ * run() will pack the scalar test cases into smaller set of vectorized tests
+ * if `cfg.vectorize` is defined.
+ * @param t the GPUTest
+ * @param expressionBuilder the expression builder function
+ * @param parameterTypes the list of expression parameter types
+ * @param returnType the return type for the expression overload
+ * @param cfg test configuration values
+ * @param cases list of test cases
+ */
+export async function run(
+ t: GPUTest,
+ expressionBuilder: ExpressionBuilder,
+ parameterTypes: Array<Type>,
+ returnType: Type,
+ cfg: Config = { inputSource: 'storage_r' },
+ cases: CaseList
+) {
+ // If the 'vectorize' config option was provided, pack the cases into vectors.
+ if (cfg.vectorize !== undefined) {
+ const packed = packScalarsToVector(parameterTypes, returnType, cases, cfg.vectorize);
+ cases = packed.cases;
+ parameterTypes = packed.parameterTypes;
+ returnType = packed.returnType;
+ }
+
+ // The size of the input buffer may exceed the maximum buffer binding size,
+ // so chunk the tests up into batches that fit into the limits. We also split
+ // the cases into smaller batches to help with shader compilation performance.
+ const casesPerBatch = (function () {
+ switch (cfg.inputSource) {
+ case 'const':
+ // Some drivers are slow to optimize shaders with many constant values,
+ // or statements. 32 is an empirically picked number of cases that works
+ // well for most drivers.
+ return 32;
+ case 'uniform':
+ // Some drivers are slow to build pipelines with large uniform buffers.
+ // 2k appears to be a sweet-spot when benchmarking.
+ return Math.floor(
+ Math.min(1024 * 2, t.device.limits.maxUniformBufferBindingSize) /
+ (parameterTypes.length * kValueStride)
+ );
+ case 'storage_r':
+ case 'storage_rw':
+ return Math.floor(
+ t.device.limits.maxStorageBufferBindingSize / (parameterTypes.length * kValueStride)
+ );
+ }
+ })();
+
+ // A cache to hold built shader pipelines.
+ const pipelineCache = new Map<String, GPUComputePipeline>();
+
+ // Submit all the cases in batches, each in a separate error scope.
+ const checkResults: Array<Promise<void>> = [];
+ for (let i = 0; i < cases.length; i += casesPerBatch) {
+ const batchCases = cases.slice(i, Math.min(i + casesPerBatch, cases.length));
+
+ t.device.pushErrorScope('validation');
+
+ const checkBatch = submitBatch(
+ t,
+ expressionBuilder,
+ parameterTypes,
+ returnType,
+ batchCases,
+ cfg.inputSource,
+ pipelineCache
+ );
+
+ checkResults.push(
+ // Check GPU validation (shader compilation, pipeline creation, etc) before checking the batch results.
+ t.device.popErrorScope().then(error => {
+ if (error === null) {
+ checkBatch();
+ } else {
+ t.fail(error.message);
+ }
+ })
+ );
+ }
+
+ // Check the results
+ await Promise.all(checkResults);
+}
+
+/**
+ * Submits the list of expression tests. The input data must fit within the
+ * buffer binding limits of the given inputSource.
+ * @param t the GPUTest
+ * @param expressionBuilder the expression builder function
+ * @param parameterTypes the list of expression parameter types
+ * @param returnType the return type for the expression overload
+ * @param cases list of test cases that fit within the binding limits of the device
+ * @param inputSource the source of the input values
+ * @param pipelineCache the cache of compute pipelines, shared between batches
+ * @returns a function that checks the results are as expected
+ */
+function submitBatch(
+ t: GPUTest,
+ expressionBuilder: ExpressionBuilder,
+ parameterTypes: Array<Type>,
+ returnType: Type,
+ cases: CaseList,
+ inputSource: InputSource,
+ pipelineCache: PipelineCache
+): () => void {
+ // Construct a buffer to hold the results of the expression tests
+ const outputBufferSize = cases.length * kValueStride;
+ const outputBuffer = t.device.createBuffer({
+ size: outputBufferSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ });
+
+ const [pipeline, group] = buildPipeline(
+ t,
+ expressionBuilder,
+ parameterTypes,
+ returnType,
+ cases,
+ inputSource,
+ outputBuffer,
+ pipelineCache
+ );
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, group);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+
+ // Heartbeat to ensure CTS runners know we're alive.
+ globalTestConfig.testHeartbeatCallback();
+
+ t.queue.submit([encoder.finish()]);
+
+ // Return a function that can check the results of the shader
+ return () => {
+ const checkExpectation = (outputData: Uint8Array) => {
+ // Read the outputs from the output buffer
+ const outputs = new Array<Value>(cases.length);
+ for (let i = 0; i < cases.length; i++) {
+ outputs[i] = returnType.read(outputData, i * kValueStride);
+ }
+
+ // The list of expectation failures
+ const errs: string[] = [];
+
+ // For each case...
+ for (let caseIdx = 0; caseIdx < cases.length; caseIdx++) {
+ const c = cases[caseIdx];
+ const got = outputs[caseIdx];
+ const cmp = toComparator(c.expected)(got);
+ if (!cmp.matched) {
+ errs.push(`(${c.input instanceof Array ? c.input.join(', ') : c.input})
+ returned: ${cmp.got}
+ expected: ${cmp.expected}`);
+ }
+ }
+
+ return errs.length > 0 ? new Error(errs.join('\n\n')) : undefined;
+ };
+
+ // Heartbeat to ensure CTS runners know we're alive.
+ globalTestConfig.testHeartbeatCallback();
+
+ t.expectGPUBufferValuesPassCheck(outputBuffer, checkExpectation, {
+ type: Uint8Array,
+ typedLength: outputBufferSize,
+ });
+ };
+}
+
+/**
+ * @param v either an array of T or a single element of type T
+ * @param i the value index to
+ * @returns the i'th value of v, if v is an array, otherwise v (i must be 0)
+ */
+function ith<T>(v: T | T[], i: number): T {
+ if (v instanceof Array) {
+ assert(i < v.length);
+ return v[i];
+ }
+ assert(i === 0);
+ return v;
+}
+
+/**
+ * Constructs and returns a GPUComputePipeline and GPUBindGroup for running a
+ * batch of test cases. If a pre-created pipeline can be found in
+ * @p pipelineCache, then this may be returned instead of creating a new
+ * pipeline.
+ * @param t the GPUTest
+ * @param expressionBuilder the expression builder function
+ * @param parameterTypes the list of expression parameter types
+ * @param returnType the return type for the expression overload
+ * @param cases list of test cases that fit within the binding limits of the device
+ * @param inputSource the source of the input values
+ * @param outputBuffer the buffer that will hold the output values of the tests
+ * @param pipelineCache the cache of compute pipelines, shared between batches
+ */
+function buildPipeline(
+ t: GPUTest,
+ expressionBuilder: ExpressionBuilder,
+ parameterTypes: Array<Type>,
+ returnType: Type,
+ cases: CaseList,
+ inputSource: InputSource,
+ outputBuffer: GPUBuffer,
+ pipelineCache: PipelineCache
+): [GPUComputePipeline, GPUBindGroup] {
+ // wgsl declaration of output buffer and binding
+ const wgslStorageType = storageType(returnType);
+ const wgslOutputs = `
+struct Output {
+ @size(${kValueStride}) value : ${wgslStorageType}
+};
+@group(0) @binding(0) var<storage, read_write> outputs : array<Output, ${cases.length}>;
+`;
+
+ switch (inputSource) {
+ case 'const': {
+ //////////////////////////////////////////////////////////////////////////
+ // Input values are constant values in the WGSL shader
+ //////////////////////////////////////////////////////////////////////////
+ const wgslValues = cases.map(c => {
+ const args = parameterTypes.map((_, i) => `(${ith(c.input, i).wgsl()})`);
+ return `${toStorage(returnType, expressionBuilder(args))}`;
+ });
+
+ const wgslBody = globalTestConfig.unrollConstEvalLoops
+ ? wgslValues.map((_, i) => `outputs[${i}].value = values[${i}];`).join('\n ')
+ : `for (var i = 0u; i < ${cases.length}; i++) {
+ outputs[i].value = values[i];
+ }`;
+
+ // the full WGSL shader source
+ const source = `
+${wgslOutputs}
+
+const values = array<${wgslStorageType}, ${cases.length}>(
+ ${wgslValues.join(',\n ')}
+);
+
+@compute @workgroup_size(1)
+fn main() {
+ ${wgslBody}
+}
+`;
+
+ // build the shader module
+ const module = t.device.createShaderModule({ code: source });
+
+ // build the pipeline
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+
+ // build the bind group
+ const group = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: outputBuffer } }],
+ });
+
+ return [pipeline, group];
+ }
+
+ case 'uniform':
+ case 'storage_r':
+ case 'storage_rw': {
+ //////////////////////////////////////////////////////////////////////////
+ // Input values come from a uniform or storage buffer
+ //////////////////////////////////////////////////////////////////////////
+
+ // returns the WGSL expression to load the ith parameter of the given type from the input buffer
+ const paramExpr = (ty: Type, i: number) => fromStorage(ty, `inputs[i].param${i}`);
+
+ // resolves to the expression that calls the builtin
+ const expr = toStorage(returnType, expressionBuilder(parameterTypes.map(paramExpr)));
+
+ // input binding var<...> declaration
+ const wgslInputVar = (function () {
+ switch (inputSource) {
+ case 'storage_r':
+ return 'var<storage, read>';
+ case 'storage_rw':
+ return 'var<storage, read_write>';
+ case 'uniform':
+ return 'var<uniform>';
+ }
+ })();
+
+ // the full WGSL shader source
+ const source = `
+struct Input {
+${parameterTypes
+ .map((ty, i) => ` @size(${kValueStride}) param${i} : ${storageType(ty)},`)
+ .join('\n')}
+};
+
+${wgslOutputs}
+
+@group(0) @binding(1)
+${wgslInputVar} inputs : array<Input, ${cases.length}>;
+
+@compute @workgroup_size(1)
+fn main() {
+ for(var i = 0; i < ${cases.length}; i++) {
+ outputs[i].value = ${expr};
+ }
+}
+`;
+
+ // size in bytes of the input buffer
+ const inputSize = cases.length * parameterTypes.length * kValueStride;
+
+ // Holds all the parameter values for all cases
+ const inputData = new Uint8Array(inputSize);
+
+ // Pack all the input parameter values into the inputData buffer
+ {
+ const caseStride = kValueStride * parameterTypes.length;
+ for (let caseIdx = 0; caseIdx < cases.length; caseIdx++) {
+ const caseBase = caseIdx * caseStride;
+ for (let paramIdx = 0; paramIdx < parameterTypes.length; paramIdx++) {
+ const offset = caseBase + paramIdx * kValueStride;
+ const params = cases[caseIdx].input;
+ if (params instanceof Array) {
+ params[paramIdx].copyTo(inputData, offset);
+ } else {
+ params.copyTo(inputData, offset);
+ }
+ }
+ }
+ }
+
+ // build the compute pipeline, if the shader hasn't been compiled already.
+ const pipeline = getOrCreate(pipelineCache, source, () => {
+ // build the shader module
+ const module = t.device.createShaderModule({ code: source });
+
+ // build the pipeline
+ return t.device.createComputePipeline({
+ layout: 'auto',
+ compute: { module, entryPoint: 'main' },
+ });
+ });
+
+ // build the input buffer
+ const inputBuffer = t.makeBufferWithContents(
+ inputData,
+ GPUBufferUsage.COPY_SRC |
+ (inputSource === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE)
+ );
+
+ // build the bind group
+ const group = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: { buffer: outputBuffer } },
+ { binding: 1, resource: { buffer: inputBuffer } },
+ ],
+ });
+
+ return [pipeline, group];
+ }
+ }
+}
+
+/**
+ * Packs a list of scalar test cases into a smaller list of vector cases.
+ * Requires that all parameters of the expression overload are of a scalar type,
+ * and the return type of the expression overload is also a scalar type.
+ * If `cases.length` is not a multiple of `vectorWidth`, then the last scalar
+ * test case value is repeated to fill the vector value.
+ */
+function packScalarsToVector(
+ parameterTypes: Array<Type>,
+ returnType: Type,
+ cases: CaseList,
+ vectorWidth: number
+): { cases: CaseList; parameterTypes: Array<Type>; returnType: Type } {
+ // Validate that the parameters and return type are all vectorizable
+ for (let i = 0; i < parameterTypes.length; i++) {
+ const ty = parameterTypes[i];
+ if (!(ty instanceof ScalarType)) {
+ throw new Error(
+ `packScalarsToVector() can only be used on scalar parameter types, but the ${i}'th parameter type is a ${ty}'`
+ );
+ }
+ }
+ if (!(returnType instanceof ScalarType)) {
+ throw new Error(
+ `packScalarsToVector() can only be used with a scalar return type, but the return type is a ${returnType}'`
+ );
+ }
+
+ const packedCases: Array<Case> = [];
+ const packedParameterTypes = parameterTypes.map(p => TypeVec(vectorWidth, p as ScalarType));
+ const packedReturnType = new VectorType(vectorWidth, returnType);
+
+ const clampCaseIdx = (idx: number) => Math.min(idx, cases.length - 1);
+
+ let caseIdx = 0;
+ while (caseIdx < cases.length) {
+ // Construct the vectorized inputs from the scalar cases
+ const packedInputs = new Array<Vector>(parameterTypes.length);
+ for (let paramIdx = 0; paramIdx < parameterTypes.length; paramIdx++) {
+ const inputElements = new Array<Scalar>(vectorWidth);
+ for (let i = 0; i < vectorWidth; i++) {
+ const input = cases[clampCaseIdx(caseIdx + i)].input;
+ inputElements[i] = (input instanceof Array ? input[paramIdx] : input) as Scalar;
+ }
+ packedInputs[paramIdx] = new Vector(inputElements);
+ }
+
+ // Gather the comparators for the packed cases
+ const comparators = new Array<Comparator>(vectorWidth);
+ for (let i = 0; i < vectorWidth; i++) {
+ comparators[i] = toComparator(cases[clampCaseIdx(caseIdx + i)].expected);
+ }
+ const packedComparator = (got: Value) => {
+ let matched = true;
+ const gElements = new Array<string>(vectorWidth);
+ const eElements = new Array<string>(vectorWidth);
+ for (let i = 0; i < vectorWidth; i++) {
+ const d = comparators[i]((got as Vector).elements[i]);
+ matched = matched && d.matched;
+ gElements[i] = d.got;
+ eElements[i] = d.expected;
+ }
+ return {
+ matched,
+ got: `${packedReturnType}(${gElements.join(', ')})`,
+ expected: `${packedReturnType}(${eElements.join(', ')})`,
+ };
+ };
+
+ // Append the new packed case
+ packedCases.push({ input: packedInputs, expected: packedComparator });
+ caseIdx += vectorWidth;
+ }
+
+ return {
+ cases: packedCases,
+ parameterTypes: packedParameterTypes,
+ returnType: packedReturnType,
+ };
+}
+
+/**
+ * Indicates bounds that acceptance intervals need to be within to avoid inputs
+ * being filtered out. This is used for const-eval tests, since going OOB will
+ * cause a validation error not an execution error.
+ */
+export type IntervalFilter =
+ | 'f32-only' // Expected to be f32 finite
+ | 'unfiltered'; // No expectations
+
+/**
+ * @returns a Case for the param and unary interval generator provided
+ * The Case will use use an interval comparator for matching results.
+ * @param param the param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for an
+ * unary operation
+ */
+function makeUnaryToF32IntervalCase(
+ param: number,
+ filter: IntervalFilter,
+ ...ops: PointToInterval[]
+): Case | undefined {
+ param = quantizeToF32(param);
+
+ const intervals = ops.map(o => o(param));
+ if (filter === 'f32-only' && intervals.some(i => !i.isFinite())) {
+ return undefined;
+ }
+ return { input: [f32(param)], expected: anyOf(...intervals) };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param params array of inputs to try
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for an
+ * unary operation
+ */
+export function generateUnaryToF32IntervalCases(
+ params: number[],
+ filter: IntervalFilter,
+ ...ops: PointToInterval[]
+): Case[] {
+ return params.reduce((cases, e) => {
+ const c = makeUnaryToF32IntervalCase(e, filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the params and binary interval generator provided
+ * The Case will use use an interval comparator for matching results.
+ * @param param0 the first param or left hand side to pass in
+ * @param param1 the second param or rhs hand side to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * binary operation
+ */
+function makeBinaryToF32IntervalCase(
+ param0: number,
+ param1: number,
+ filter: IntervalFilter,
+ ...ops: BinaryToInterval[]
+): Case | undefined {
+ param0 = quantizeToF32(param0);
+ param1 = quantizeToF32(param1);
+
+ const intervals = ops.map(o => o(param0, param1));
+ if (filter === 'f32-only' && intervals.some(i => !i.isFinite())) {
+ return undefined;
+ }
+ return { input: [f32(param0), f32(param1)], expected: anyOf(...intervals) };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first param
+ * @param param1s array of inputs to try for the second param
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * binary operation
+ */
+export function generateBinaryToF32IntervalCases(
+ param0s: number[],
+ param1s: number[],
+ filter: IntervalFilter,
+ ...ops: BinaryToInterval[]
+): Case[] {
+ return cartesianProduct(param0s, param1s).reduce((cases, e) => {
+ const c = makeBinaryToF32IntervalCase(e[0], e[1], filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the params and ternary interval generator provided
+ * The Case will use use an interval comparator for matching results.
+ * @param param0 the first param to pass in
+ * @param param1 the second param to pass in
+ * @param param2 the third param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * ternary operation.
+ */
+function makeTernaryToF32IntervalCase(
+ param0: number,
+ param1: number,
+ param2: number,
+ filter: IntervalFilter,
+ ...ops: TernaryToInterval[]
+): Case | undefined {
+ param0 = quantizeToF32(param0);
+ param1 = quantizeToF32(param1);
+ param2 = quantizeToF32(param2);
+
+ const intervals = ops.map(o => o(param0, param1, param2));
+ if (filter === 'f32-only' && intervals.some(i => !i.isFinite())) {
+ return undefined;
+ }
+ return {
+ input: [f32(param0), f32(param1), f32(param2)],
+ expected: anyOf(...intervals),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first param
+ * @param param1s array of inputs to try for the second param
+ * @param param2s array of inputs to try for the third param
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * ternary operation.
+ */
+export function generateTernaryToF32IntervalCases(
+ param0s: number[],
+ param1s: number[],
+ param2s: number[],
+ filter: IntervalFilter,
+ ...ops: TernaryToInterval[]
+): Case[] {
+ return cartesianProduct(param0s, param1s, param2s).reduce((cases, e) => {
+ const c = makeTernaryToF32IntervalCase(e[0], e[1], e[2], filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the param and vector interval generator provided
+ * @param param the param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * vector.
+ */
+function makeVectorToF32IntervalCase(
+ param: number[],
+ filter: IntervalFilter,
+ ...ops: VectorToInterval[]
+): Case | undefined {
+ param = param.map(quantizeToF32);
+ const param_f32 = param.map(f32);
+
+ const intervals = ops.map(o => o(param));
+ if (filter === 'f32-only' && intervals.some(i => !i.isFinite())) {
+ return undefined;
+ }
+ return {
+ input: [new Vector(param_f32)],
+ expected: anyOf(...intervals),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param params array of inputs to try
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * vector.
+ */
+export function generateVectorToF32IntervalCases(
+ params: number[][],
+ filter: IntervalFilter,
+ ...ops: VectorToInterval[]
+): Case[] {
+ return params.reduce((cases, e) => {
+ const c = makeVectorToF32IntervalCase(e, filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the params and vector pair interval generator provided
+ * @param param0 the first param to pass in
+ * @param param1 the second param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * pair of vectors.
+ */
+function makeVectorPairToF32IntervalCase(
+ param0: number[],
+ param1: number[],
+ filter: IntervalFilter,
+ ...ops: VectorPairToInterval[]
+): Case | undefined {
+ param0 = param0.map(quantizeToF32);
+ param1 = param1.map(quantizeToF32);
+ const param0_f32 = param0.map(f32);
+ const param1_f32 = param1.map(f32);
+
+ const intervals = ops.map(o => o(param0, param1));
+ if (filter === 'f32-only' && intervals.some(i => !i.isFinite())) {
+ return undefined;
+ }
+ return {
+ input: [new Vector(param0_f32), new Vector(param1_f32)],
+ expected: anyOf(...intervals),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first input
+ * @param param1s array of inputs to try for the second input
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance interval for a
+ * pair of vectors.
+ */
+export function generateVectorPairToF32IntervalCases(
+ param0s: number[][],
+ param1s: number[][],
+ filter: IntervalFilter,
+ ...ops: VectorPairToInterval[]
+): Case[] {
+ return cartesianProduct(param0s, param1s).reduce((cases, e) => {
+ const c = makeVectorPairToF32IntervalCase(e[0], e[1], filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the param and vector of intervals generator provided
+ * @param param the param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an vector of acceptance
+ * intervals for a vector.
+ */
+function makeVectorToVectorCase(
+ param: number[],
+ filter: IntervalFilter,
+ ...ops: VectorToVector[]
+): Case | undefined {
+ param = param.map(quantizeToF32);
+ const param_f32 = param.map(f32);
+
+ const vectors = ops.map(o => o(param));
+ if (filter === 'f32-only' && vectors.some(v => !v.every(e => e.isFinite()))) {
+ return undefined;
+ }
+ return {
+ input: [new Vector(param_f32)],
+ expected: anyOf(...vectors),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param params array of inputs to try
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an vector of acceptance
+ * intervals for a vector.
+ */
+export function generateVectorToVectorCases(
+ params: number[][],
+ filter: IntervalFilter,
+ ...ops: VectorToVector[]
+): Case[] {
+ return params.reduce((cases, e) => {
+ const c = makeVectorToVectorCase(e, filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the params and vector of intervals generator provided
+ * @param param0 the first param to pass in
+ * @param param1 the second param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an vector of acceptance
+ * intervals for a pair of vectors.
+ */
+function makeVectorPairToVectorCase(
+ param0: number[],
+ param1: number[],
+ filter: IntervalFilter,
+ ...ops: VectorPairToVector[]
+): Case | undefined {
+ param0 = param0.map(quantizeToF32);
+ param1 = param1.map(quantizeToF32);
+ const param0_f32 = param0.map(f32);
+ const param1_f32 = param1.map(f32);
+
+ const vectors = ops.map(o => o(param0, param1));
+ if (filter === 'f32-only' && vectors.some(v => !v.every(e => e.isFinite()))) {
+ return undefined;
+ }
+ return {
+ input: [new Vector(param0_f32), new Vector(param1_f32)],
+ expected: anyOf(...vectors),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first input
+ * @param param1s array of inputs to try for the second input
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an vector of acceptance
+ * intervals for a pair of vectors.
+ */
+export function generateVectorPairToVectorCases(
+ param0s: number[][],
+ param1s: number[][],
+ filter: IntervalFilter,
+ ...ops: VectorPairToVector[]
+): Case[] {
+ return cartesianProduct(param0s, param1s).reduce((cases, e) => {
+ const c = makeVectorPairToVectorCase(e[0], e[1], filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * @returns a Case for the param and vector of intervals generator provided
+ * The input is treated as an unsigned int.
+ * @param param the param to pass in
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance
+ * interval for an unsigned int.
+ */
+function makeU32ToVectorCase(
+ param: number,
+ filter: IntervalFilter,
+ ...ops: PointToVector[]
+): Case | undefined {
+ param = Math.trunc(param);
+ const param_u32 = u32(param);
+
+ const vectors = ops.map(o => o(param));
+ if (filter === 'f32-only' && vectors.some(v => !v.every(e => e.isFinite()))) {
+ return undefined;
+ }
+ return {
+ input: param_u32,
+ expected: anyOf(...vectors),
+ };
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * The input is treated as an unsigned int.
+ * @param params array of inputs to try
+ * @param filter what interval filtering to apply
+ * @param ops callbacks that implement generating an acceptance
+ * interval for an unsigned int.
+ */
+export function generateU32ToVectorCases(
+ params: number[],
+ filter: IntervalFilter,
+ ...ops: PointToVector[]
+): Case[] {
+ return params.reduce((cases, e) => {
+ const c = makeU32ToVectorCase(e, filter, ...ops);
+ if (c !== undefined) {
+ cases.push(c);
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * A function that performs a binary operation on x and y, and returns the expected
+ * result, or undefined if the operation is invalid for the given inputs.
+ */
+export interface BinaryToI32Op {
+ (x: number, y: number): number | undefined;
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first param
+ * @param param1s array of inputs to try for the second param
+ * @param op callback called on each pair of inputs to produce each case
+ */
+export function generateBinaryToI32Cases(
+ params0s: number[],
+ params1s: number[],
+ op: BinaryToI32Op
+) {
+ return cartesianProduct(params0s, params1s).reduce((cases, e) => {
+ const expected = op(e[0], e[1]);
+ if (expected !== undefined) {
+ cases.push({ input: [i32(e[0]), i32(e[1])], expected: i32(expected) });
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+export interface BinaryToU32Op {
+ (x: number, y: number): number | undefined;
+}
+
+/**
+ * @returns an array of Cases for operations over a range of inputs
+ * @param param0s array of inputs to try for the first param
+ * @param param1s array of inputs to try for the second param
+ * @param op callback called on each pair of inputs to produce each case
+ */
+export function generateBinaryToU32Cases(
+ params0s: number[],
+ params1s: number[],
+ op: BinaryToU32Op
+) {
+ return cartesianProduct(params0s, params1s).reduce((cases, e) => {
+ const expected = op(e[0], e[1]);
+ if (expected !== undefined) {
+ cases.push({ input: [u32(e[0]), u32(e[1])], expected: u32(expected) });
+ }
+ return cases;
+ }, new Array<Case>());
+}
+
+/**
+ * A function that performs a binary operation on x and y, and returns the expected
+ * result.
+ */
+export interface BinaryOp {
+ (x: number, y: number): number;
+}
+
+/**
+ * @returns a Case for the input params with op applied
+ * @param scalar scalar param
+ * @param vector vector param (2, 3, or 4 elements)
+ * @param op the op to apply to scalar and vector
+ */
+function makeU32VectorBinaryToVectorCase(scalar: number, vector: number[], op: BinaryOp): Case {
+ scalar = quantizeToU32(scalar);
+ vector = vector.map(quantizeToU32);
+ const result = new Vector(vector.map(v => u32(op(scalar, v))));
+ return {
+ input: [u32(scalar), new Vector(vector.map(u32))],
+ expected: result,
+ };
+}
+
+/**
+ * @returns array of Case for the input params with op applied
+ * @param scalars array of scalar params
+ * @param vectors array of vector params (2, 3, or 4 elements)
+ * @param op he op to apply to each pair of scalar and vector
+ */
+export function generateU32VectorBinaryToVectorCases(
+ scalars: number[],
+ vectors: number[][],
+ op: BinaryOp
+): Case[] {
+ return scalars.flatMap(s => {
+ return vectors.map(v => {
+ return makeU32VectorBinaryToVectorCase(s, v, op);
+ });
+ });
+}
+
+/**
+ * @returns a Case for the input params with op applied
+ * @param vector vector param (2, 3, or 4 elements)
+ * @param scalar scalar param
+ * @param op the op to apply to vector and scalar
+ */
+function makeVectorU32BinaryToVectorCase(vector: number[], scalar: number, op: BinaryOp): Case {
+ vector = vector.map(quantizeToU32);
+ scalar = quantizeToU32(scalar);
+ const result = new Vector(vector.map(v => u32(op(v, scalar))));
+ return {
+ input: [new Vector(vector.map(u32)), u32(scalar)],
+ expected: result,
+ };
+}
+
+/**
+ * @returns array of Case for the input params with op applied
+ * @param vectors array of vector params (2, 3, or 4 elements)
+ * @param scalars array of scalar params
+ * @param op he op to apply to each pair of vector and scalar
+ */
+export function generateVectorU32BinaryToVectorCases(
+ vectors: number[][],
+ scalars: number[],
+ op: BinaryOp
+): Case[] {
+ return scalars.flatMap(s => {
+ return vectors.map(v => {
+ return makeVectorU32BinaryToVectorCase(v, s, op);
+ });
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts
new file mode 100644
index 0000000000..01eaaab43a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/bool_logical.spec.ts
@@ -0,0 +1,33 @@
+export const description = `
+Execution Tests for the boolean unary logical expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { bool, TypeBool } from '../../../../util/conversion.js';
+import { allInputSources, run } from '../expression.js';
+
+import { unary } from './unary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('negation')
+ .specURL('https://www.w3.org/TR/WGSL/#logical-expr')
+ .desc(
+ `
+Expression: !e
+
+Logical negation. The result is true when e is false and false when e is true. Component-wise when T is a vector.
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = [
+ { input: bool(true), expected: bool(false) },
+ { input: bool(false), expected: bool(true) },
+ ];
+
+ await run(t, unary('!'), [TypeBool], TypeBool, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts
new file mode 100644
index 0000000000..c5642b5351
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/f32_arithmetic.spec.ts
@@ -0,0 +1,41 @@
+export const description = `
+Execution Tests for the f32 arithmetic unary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { TypeF32 } from '../../../../util/conversion.js';
+import { negationInterval } from '../../../../util/f32_interval.js';
+import { fullF32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import { allInputSources, generateUnaryToF32IntervalCases, run } from '../expression.js';
+
+import { unary } from './unary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unary/f32_arithmetic', {
+ negation: () => {
+ return generateUnaryToF32IntervalCases(
+ fullF32Range({ neg_norm: 250, neg_sub: 20, pos_sub: 20, pos_norm: 250 }),
+ 'unfiltered',
+ negationInterval
+ );
+ },
+});
+
+g.test('negation')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: -x
+Accuracy: Correctly rounded
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('negation');
+ await run(t, unary('-'), [TypeF32], TypeF32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts
new file mode 100644
index 0000000000..14519b8967
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/i32_arithmetic.spec.ts
@@ -0,0 +1,37 @@
+export const description = `
+Execution Tests for the i32 arithmetic unary expression operations
+`;
+
+import { makeTestGroup } from '../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../gpu_test.js';
+import { i32, TypeI32 } from '../../../../util/conversion.js';
+import { fullI32Range } from '../../../../util/math.js';
+import { makeCaseCache } from '../case_cache.js';
+import { allInputSources, run } from '../expression.js';
+
+import { unary } from './unary.js';
+
+export const g = makeTestGroup(GPUTest);
+
+export const d = makeCaseCache('unary/i32_arithmetic', {
+ negation: () => {
+ return fullI32Range().map(e => {
+ return { input: i32(e), expected: i32(-e) };
+ });
+ },
+});
+
+g.test('negation')
+ .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation')
+ .desc(
+ `
+Expression: -x
+`
+ )
+ .params(u =>
+ u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const)
+ )
+ .fn(async t => {
+ const cases = await d.get('negation');
+ await run(t, unary('-'), [TypeI32], TypeI32, t.params, cases);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts
new file mode 100644
index 0000000000..212b483e67
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/expression/unary/unary.ts
@@ -0,0 +1,6 @@
+import { ExpressionBuilder } from '../expression.js';
+
+/* @returns an ExpressionBuilder that evaluates a prefix unary operation */
+export function unary(op: string): ExpressionBuilder {
+ return value => `${op}(${value})`;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/atomicity.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/atomicity.spec.ts
new file mode 100644
index 0000000000..371eee5f92
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/atomicity.spec.ts
@@ -0,0 +1,102 @@
+export const description = `Tests for the atomicity of atomic read-modify-write instructions.`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+import {
+ MemoryModelTestParams,
+ MemoryModelTester,
+ buildTestShader,
+ TestType,
+ buildResultShader,
+ ResultType,
+ MemoryType,
+} from './memory_model_setup.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// A reasonable parameter set, determined heuristically.
+const memoryModelTestParams: MemoryModelTestParams = {
+ workgroupSize: 256,
+ testingWorkgroups: 512,
+ maxWorkgroups: 1024,
+ shufflePct: 100,
+ barrierPct: 100,
+ memStressPct: 100,
+ memStressIterations: 1024,
+ memStressStoreFirstPct: 50,
+ memStressStoreSecondPct: 50,
+ preStressPct: 100,
+ preStressIterations: 1024,
+ preStressStoreFirstPct: 50,
+ preStressStoreSecondPct: 50,
+ scratchMemorySize: 2048,
+ stressLineSize: 64,
+ stressTargetLines: 2,
+ stressStrategyBalancePct: 50,
+ permuteFirst: 109,
+ permuteSecond: 419,
+ memStride: 4,
+ aliasedMemory: false,
+ numBehaviors: 4,
+};
+
+const storageMemoryTestCode = `
+ let r0 = atomicAdd(&test_locations.value[x_0], 0u);
+ atomicStore(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupMemoryTestCode = `
+ let r0 = atomicAdd(&wg_test_locations[x_0], 0u);
+ atomicStore(&wg_test_locations[x_1], 2u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+const resultCode = `
+ if ((r0 == 0u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 2u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+`;
+
+g.test('atomicity')
+ .desc(
+ `Checks whether a store on one thread can interrupt an atomic RMW on a second thread. If the read returned by
+ the RMW instruction is the initial value of memory (0), but the final value in memory is 1, then the atomic write
+ in the second thread occurred in between the read and the write of the RMW.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: storageMemoryTestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryTestCode,
+ },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.FourBehavior);
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(10, 3);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts
new file mode 100644
index 0000000000..6cda6c3e19
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/barrier.spec.ts
@@ -0,0 +1,211 @@
+export const description = `
+Tests for non-atomic memory synchronization within a workgroup in the presence of a WebGPU barrier`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+import {
+ MemoryModelTestParams,
+ MemoryModelTester,
+ buildTestShader,
+ MemoryType,
+ TestType,
+ buildResultShader,
+ ResultType,
+} from './memory_model_setup.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// A reasonable parameter set, determined heuristically.
+const memoryModelTestParams: MemoryModelTestParams = {
+ workgroupSize: 256,
+ testingWorkgroups: 512,
+ maxWorkgroups: 1024,
+ shufflePct: 100,
+ barrierPct: 100,
+ memStressPct: 100,
+ memStressIterations: 1024,
+ memStressStoreFirstPct: 50,
+ memStressStoreSecondPct: 50,
+ preStressPct: 100,
+ preStressIterations: 1024,
+ preStressStoreFirstPct: 50,
+ preStressStoreSecondPct: 50,
+ scratchMemorySize: 2048,
+ stressLineSize: 64,
+ stressTargetLines: 2,
+ stressStrategyBalancePct: 50,
+ permuteFirst: 109,
+ permuteSecond: 419,
+ memStride: 4,
+ aliasedMemory: false,
+ numBehaviors: 2,
+};
+
+const storageMemoryBarrierStoreLoadTestCode = `
+ test_locations.value[x_0] = 1u;
+ workgroupBarrier();
+ let r0 = test_locations.value[x_1];
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+`;
+
+const workgroupMemoryBarrierStoreLoadTestCode = `
+ wg_test_locations[x_0] = 1u;
+ workgroupBarrier();
+ let r0 = wg_test_locations[x_1];
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+`;
+
+g.test('workgroup_barrier_store_load')
+ .desc(
+ `Checks whether the workgroup barrier properly synchronizes a non-atomic write and read on
+ separate threads in the same workgroup. Within a workgroup, the barrier should force an invocation
+ after the barrier to read a write from an invocation before the barrier.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.NonAtomicStorageClass, _testCode: storageMemoryBarrierStoreLoadTestCode },
+ {
+ memType: MemoryType.NonAtomicWorkgroupClass,
+ _testCode: workgroupMemoryBarrierStoreLoadTestCode,
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if (r0 == 1u) {
+ atomicAdd(&test_results.seq, 1u);
+ } else if (r0 == 0u) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const resultShader = buildResultShader(
+ resultCode,
+ TestType.IntraWorkgroup,
+ ResultType.TwoBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(15, 1);
+ });
+
+const storageMemoryBarrierLoadStoreTestCode = `
+ let r0 = test_locations.value[x_0];
+ workgroupBarrier();
+ test_locations.value[x_1] = 1u;
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const workgroupMemoryBarrierLoadStoreTestCode = `
+ let r0 = wg_test_locations[x_0];
+ workgroupBarrier();
+ wg_test_locations[x_1] = 1u;
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+g.test('workgroup_barrier_load_store')
+ .desc(
+ `Checks whether the workgroup barrier properly synchronizes a non-atomic write and read on
+ separate threads in the same workgroup. Within a workgroup, the barrier should force an invocation
+ before the barrier to not read the write from an invocation after the barrier.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.NonAtomicStorageClass, _testCode: storageMemoryBarrierLoadStoreTestCode },
+ {
+ memType: MemoryType.NonAtomicWorkgroupClass,
+ _testCode: workgroupMemoryBarrierLoadStoreTestCode,
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if (r0 == 0u) {
+ atomicAdd(&test_results.seq, 1u);
+ } else if (r0 == 1u) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const resultShader = buildResultShader(
+ resultCode,
+ TestType.IntraWorkgroup,
+ ResultType.TwoBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(12, 1);
+ });
+
+const storageMemoryBarrierStoreStoreTestCode = `
+ test_locations.value[x_0] = 1u;
+ storageBarrier();
+ test_locations.value[x_1] = 2u;
+`;
+
+const workgroupMemoryBarrierStoreStoreTestCode = `
+ wg_test_locations[x_0] = 1u;
+ workgroupBarrier();
+ wg_test_locations[x_1] = 2u;
+ workgroupBarrier();
+ test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1] = wg_test_locations[x_1];
+`;
+
+g.test('workgroup_barrier_store_store')
+ .desc(
+ `Checks whether the workgroup barrier properly synchronizes non-atomic writes on
+ separate threads in the same workgroup. Within a workgroup, the barrier should force the value in memory
+ to be the result of the write after the barrier, not the write before.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.NonAtomicStorageClass,
+ _testCode: storageMemoryBarrierStoreStoreTestCode,
+ },
+ {
+ memType: MemoryType.NonAtomicWorkgroupClass,
+ _testCode: workgroupMemoryBarrierStoreStoreTestCode,
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if (mem_x_0 == 2u) {
+ atomicAdd(&test_results.seq, 1u);
+ } else if (mem_x_0 == 1u) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const resultShader = buildResultShader(
+ resultCode,
+ TestType.IntraWorkgroup,
+ ResultType.TwoBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(10, 1);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/coherence.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/coherence.spec.ts
new file mode 100644
index 0000000000..742db51169
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/coherence.spec.ts
@@ -0,0 +1,525 @@
+export const description = `
+Tests that all threads see a sequentially consistent view of the order of memory
+accesses to a single memory location. Uses a parallel testing strategy along with stressing
+threads to increase coverage of possible bugs.`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+import {
+ MemoryModelTestParams,
+ MemoryModelTester,
+ buildTestShader,
+ MemoryType,
+ TestType,
+ buildResultShader,
+ ResultType,
+} from './memory_model_setup.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// A reasonable parameter set, determined heuristically.
+const memoryModelTestParams: MemoryModelTestParams = {
+ workgroupSize: 256,
+ testingWorkgroups: 39,
+ maxWorkgroups: 952,
+ shufflePct: 0,
+ barrierPct: 0,
+ memStressPct: 0,
+ memStressIterations: 1024,
+ memStressStoreFirstPct: 50,
+ memStressStoreSecondPct: 50,
+ preStressPct: 0,
+ preStressIterations: 1024,
+ preStressStoreFirstPct: 50,
+ preStressStoreSecondPct: 50,
+ scratchMemorySize: 2048,
+ stressLineSize: 64,
+ stressTargetLines: 2,
+ stressStrategyBalancePct: 50,
+ permuteFirst: 109,
+ permuteSecond: 1,
+ memStride: 1,
+ aliasedMemory: true,
+ numBehaviors: 4,
+};
+
+const storageMemoryCorrTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[x_1]);
+ let r1 = atomicLoad(&test_locations.value[y_1]);
+ atomicStore(&results.value[id_1].r0, r0);
+ atomicStore(&results.value[id_1].r1, r1);
+`;
+
+const workgroupStorageMemoryCorrTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[x_1]);
+ let r1 = atomicLoad(&test_locations.value[y_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const storageMemoryCorrRMWTestCode = `
+ atomicExchange(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[x_1]);
+ let r1 = atomicAdd(&test_locations.value[y_1], 0u);
+ atomicStore(&results.value[id_1].r0, r0);
+ atomicStore(&results.value[id_1].r1, r1);
+`;
+
+const workgroupStorageMemoryCorrRMWTestCode = `
+ atomicExchange(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[x_1]);
+ let r1 = atomicAdd(&test_locations.value[y_1], 0u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const workgroupMemoryCorrTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ let r0 = atomicLoad(&wg_test_locations[x_1]);
+ let r1 = atomicLoad(&wg_test_locations[y_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const workgroupMemoryCorrRMWTestCode = `
+ atomicExchange(&wg_test_locations[x_0], 1u);
+ let r0 = atomicLoad(&wg_test_locations[x_1]);
+ let r1 = atomicAdd(&wg_test_locations[y_1], 0u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+g.test('corr')
+ .desc(
+ `Ensures two reads on one thread cannot observe an inconsistent view of a write on a second thread.
+ The first thread writes the value 1 some location x, and the second thread reads x twice in a row.
+ If the first read returns 1 but the second read returns 0, then there has been a coherence violation.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCorrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCorrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCorrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCorrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCorrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCorrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if ((r0 == 0u && r1 == 0u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 1u && r1 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && r1 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 1u && r1 == 0u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.FourBehavior);
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(60, 3);
+ });
+
+const storageMemoryCowwTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ atomicStore(&test_locations.value[y_0], 2u);
+`;
+
+const storageMemoryCowwRMWTestCode = `
+ atomicExchange(&test_locations.value[x_0], 1u);
+ atomicStore(&test_locations.value[y_0], 2u);
+`;
+
+const workgroupMemoryCowwTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ atomicStore(&wg_test_locations[y_0], 2u);
+ workgroupBarrier();
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_0], atomicLoad(&wg_test_locations[x_0]));
+`;
+
+const workgroupMemoryCowwRMWTestCode = `
+ atomicExchange(&wg_test_locations[x_0], 1u);
+ atomicStore(&wg_test_locations[y_0], 2u);
+ workgroupBarrier();
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_0], atomicLoad(&wg_test_locations[x_0]));
+`;
+
+g.test('coww')
+ .desc(
+ `Ensures two writes on one thread do not lead to incoherent results. The thread first writes 1 to
+ some location x and then writes 2 to the same location. If the value in memory after the test finishes
+ is 1, then there has been a coherence violation.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCowwTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCowwRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: storageMemoryCowwTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: storageMemoryCowwRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCowwTestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCowwRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if (mem_x_0 == 2u) {
+ atomicAdd(&test_results.seq, 1u);
+ } else if (mem_x_0 == 1u) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.TwoBehavior);
+ const params = {
+ ...memoryModelTestParams,
+ numBehaviors: 2,
+ };
+ const memModelTester = new MemoryModelTester(t, params, testShader, resultShader);
+ await memModelTester.run(60, 1);
+ });
+
+const storageMemoryCowrTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[y_0]);
+ atomicStore(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupStorageMemoryCowrTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[y_0]);
+ atomicStore(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const storageMemoryCowrRMWTestCode = `
+ atomicExchange(&test_locations.value[x_0], 1u);
+ let r0 = atomicAdd(&test_locations.value[y_0], 0u);
+ atomicExchange(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupStorageMemoryCowrRMWTestCode = `
+ atomicExchange(&test_locations.value[x_0], 1u);
+ let r0 = atomicAdd(&test_locations.value[y_0], 0u);
+ atomicExchange(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const workgroupMemoryCowrTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ let r0 = atomicLoad(&wg_test_locations[y_0]);
+ atomicStore(&wg_test_locations[x_1], 2u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+const workgroupMemoryCowrRMWTestCode = `
+ atomicExchange(&wg_test_locations[x_0], 1u);
+ let r0 = atomicAdd(&wg_test_locations[y_0], 0u);
+ atomicExchange(&wg_test_locations[x_1], 2u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+g.test('cowr')
+ .desc(
+ `The first thread first writes 1 to some location x and then reads x. The second thread writes 2 to x.
+ If the first thread reads the value 2 and the value in memory at the end of the test is 1, then the read
+ and write on the first thread have been reordered, a coherence violation.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCowrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCowrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCowrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCowrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCowrTestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCowrRMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if ((r0 == 1u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 1u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 2u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 2u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.FourBehavior);
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(60, 3);
+ });
+
+const storageMemoryCorw1TestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[x_0], 1u);
+ workgroupBarrier();
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupStorageMemoryCorw1TestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[y_0], 1u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const workgroupMemoryCorw1TestCode = `
+ let r0 = atomicLoad(&wg_test_locations[x_0]);
+ atomicStore(&wg_test_locations[y_0], 1u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+g.test('corw1')
+ .desc(
+ `One thread first reads from a memory location x and then writes 1 to x. If the read observes the subsequent
+ write, there has been a coherence violation.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCorw1TestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCorw1TestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCorw1TestCode,
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if (r0 == 0u) {
+ atomicAdd(&test_results.seq, 1u);
+ } else if (r0 == 1u) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.TwoBehavior);
+ const params = {
+ ...memoryModelTestParams,
+ numBehaviors: 2,
+ };
+ const memModelTester = new MemoryModelTester(t, params, testShader, resultShader);
+ await memModelTester.run(60, 1);
+ });
+
+const storageMemoryCorw2TestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[y_0], 1u);
+ atomicStore(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupStorageMemoryCorw2TestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[y_0], 1u);
+ atomicStore(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const storageMemoryCorw2RMWTestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[y_0], 1u);
+ atomicExchange(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[id_0].r0, r0);
+`;
+
+const workgroupStorageMemoryCorw2RMWTestCode = `
+ let r0 = atomicLoad(&test_locations.value[x_0]);
+ atomicStore(&test_locations.value[y_0], 1u);
+ atomicExchange(&test_locations.value[x_1], 2u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+`;
+
+const workgroupMemoryCorw2TestCode = `
+ let r0 = atomicLoad(&wg_test_locations[x_0]);
+ atomicStore(&wg_test_locations[y_0], 1u);
+ atomicStore(&wg_test_locations[x_1], 2u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+const workgroupMemoryCorw2RMWTestCode = `
+ let r0 = atomicLoad(&wg_test_locations[x_0]);
+ atomicStore(&wg_test_locations[y_0], 1u);
+ atomicExchange(&wg_test_locations[x_1], 2u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+g.test('corw2')
+ .desc(
+ `The first thread reads from some memory location x, and then writes 1 to x. The second thread
+ writes 2 to x. If the first thread reads the value 2, but the value in memory after the test
+ completes is 1, then the instructions on the first thread have been re-ordered, leading to a
+ coherence violation.
+ `
+ )
+ .paramsSimple([
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCorw2TestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.InterWorkgroup,
+ _testCode: storageMemoryCorw2RMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCorw2TestCode,
+ },
+ {
+ memType: MemoryType.AtomicStorageClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupStorageMemoryCorw2RMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCorw2TestCode,
+ },
+ {
+ memType: MemoryType.AtomicWorkgroupClass,
+ testType: TestType.IntraWorkgroup,
+ _testCode: workgroupMemoryCorw2RMWTestCode,
+ extraFlags: 'rmw_variant',
+ },
+ ])
+ .fn(async t => {
+ const resultCode = `
+ if ((r0 == 0u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 2u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 2u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `;
+ const testShader = buildTestShader(t.params._testCode, t.params.memType, t.params.testType);
+ const resultShader = buildResultShader(resultCode, t.params.testType, ResultType.FourBehavior);
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ resultShader
+ );
+ await memModelTester.run(60, 3);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts
new file mode 100644
index 0000000000..c26f8fde29
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/memory_model_setup.ts
@@ -0,0 +1,1049 @@
+import { GPUTest } from '../../../gpu_test';
+import { checkElementsPassPredicate } from '../../../util/check_contents.js';
+
+/* All buffer sizes are counted in units of 4-byte words. */
+
+/* Parameter values are set heuristically, typically by a time-intensive search. */
+export type MemoryModelTestParams = {
+ /* Number of invocations per workgroup. The workgroups are 1-dimensional. */
+ workgroupSize: number;
+ /** The number of workgroups to assign to running the test. */
+ testingWorkgroups: number;
+ /**
+ * Run no more than this many workgroups. Must be >= the number of testing workgroups. Non-testing workgroups are used
+ * to stress other memory locations.
+ */
+ maxWorkgroups: number;
+ /** The percentage of iterations to shuffle the workgroup ids. */
+ shufflePct: number;
+ /** The percentage of iterations to run the bounded spin-loop barrier. */
+ barrierPct: number;
+ /** The percentage of iterations to run memory stress using non-testing workgroups. */
+ memStressPct: number;
+ /** The number of iterations to run the memory stress pattern. */
+ memStressIterations: number;
+ /** The percentage of iterations the first instruction in the stress pattern should be a store. */
+ memStressStoreFirstPct: number;
+ /** The percentage of iterations the second instruction in the stress pattern should be a store. */
+ memStressStoreSecondPct: number;
+ /** The percentage of iterations for testing threads to run stress before running the test. */
+ preStressPct: number;
+ /** Same as for memStressIterations. */
+ preStressIterations: number;
+ /** The percentage of iterations the first instruction in the pre-stress pattern should be a store. */
+ preStressStoreFirstPct: number;
+ /** The percentage of iterations the second instruction in the pre-stress pattern should be a store. */
+ preStressStoreSecondPct: number;
+ /** The size of the scratch memory region, used for stressing threads. */
+ scratchMemorySize: number;
+ /** The size of each block of memory stressing threads access. */
+ stressLineSize: number;
+ /** The number of blocks of memory to assign stressing threads to. */
+ stressTargetLines: number;
+ /** How non-testing threads are assigned to stressing locations. 100 means all iterations use a round robin approach, 0 means all use a chunking approach. */
+ stressStrategyBalancePct: number;
+ /** Used to permute thread ids within a workgroup, so more random pairings are created between threads coordinating on a test. */
+ permuteFirst: number;
+ /** Used to create distance between memory locations used in a test. Set this to 1 for memory that should be aliased. */
+ permuteSecond: number;
+ /** The distance (in number of 4 byte intervals) between any two memory locations used for testing. */
+ memStride: number;
+ /** For tests that access one memory location, but use dynamic addresses to avoid compiler optimization, aliased memory should be set to true. */
+ aliasedMemory: boolean;
+ /** The number of possible behaviors that a test can have. */
+ numBehaviors: number;
+};
+
+/** The number of memory locations accessed by a test. Currently, only tests with up to 2 memory locations are supported. */
+const numMemLocations = 2;
+
+/** The number of read outputs per test that need to be analyzed in the result aggregation shader. Currently, only tests with up to 2 read outputs are supported. */
+const numReadOutputs = 2;
+
+/** Represents a device buffer and a utility buffer for resetting memory and copying parameters. */
+type BufferWithSource = {
+ /** Buffer used by shader code. */
+ deviceBuf: GPUBuffer;
+ /** Buffer populated from the host size, data is copied to device buffer for use by shader. */
+ srcBuf: GPUBuffer;
+ /** Size in bytes of the buffer. */
+ size: number;
+};
+
+/** Specifies the buffers used during a memory model test. */
+type MemoryModelBuffers = {
+ /** This is the memory region that testing threads read from and write to. */
+ testLocations: BufferWithSource;
+ /** This buffer collects the results of reads for analysis in the result aggregation shader. */
+ readResults: BufferWithSource;
+ /** This buffer is the aggregated results of every testing thread, and is used to check for test success/failure. */
+ testResults: BufferWithSource;
+ /** This buffer stores the shuffled workgroup ids for use during testing. Read-only in the shader. */
+ shuffledWorkgroups: BufferWithSource;
+ /** This is the bounded spin-loop barrier, used to temporally align testing threads. */
+ barrier: BufferWithSource;
+ /** Memory region for stressing threads to read to and write from. */
+ scratchpad: BufferWithSource;
+ /** The memory locations in the scratch region that stressing threads access. */
+ scratchMemoryLocations: BufferWithSource;
+ /** Parameters that are used by the shader to calculate memory locations and perform stress. */
+ stressParams: BufferWithSource;
+};
+
+/** The number of stress params to add to the stress params buffer. */
+const numStressParams = 12;
+const barrierParamIndex = 0;
+const memStressIndex = 1;
+const memStressIterationsIndex = 2;
+const memStressPatternIndex = 3;
+const preStressIndex = 4;
+const preStressIterationsIndex = 5;
+const preStressPatternIndex = 6;
+const permuteFirstIndex = 7;
+const permuteSecondIndex = 8;
+const testingWorkgroupsIndex = 9;
+const memStrideIndex = 10;
+const memLocationOffsetIndex = 11;
+
+/**
+ * All memory used in these consists of a four byte word, so this value is used to correctly set the byte size of buffers that
+ * are read to/written from during tests and for storing test results.
+ */
+const bytesPerWord = 4;
+
+/**
+ * Implements setup code necessary to run a memory model test. A test consists of two parts:
+ * 1.) A test shader that runs a specified memory model litmus test and attempts to reveal a weak (disallowed) behavior.
+ * At a high level, a test shader consists of a set of testing workgroups where every invocation executes the litmus test
+ * on a set of test locations, and a set of stressing workgroups where every invocation accesses a specified memory location
+ * in a random pattern.
+ * 2.) A result shader that takes the output of the test shader, which consists of the memory locations accessed during the test
+ * and the results of any reads made during the test, and aggregate the results based on the possible behaviors of the test.
+ */
+export class MemoryModelTester {
+ protected test: GPUTest;
+ protected params: MemoryModelTestParams;
+ protected buffers: MemoryModelBuffers;
+ protected testPipeline: GPUComputePipeline;
+ protected testBindGroup: GPUBindGroup;
+ protected resultPipeline: GPUComputePipeline;
+ protected resultBindGroup: GPUBindGroup;
+
+ /** Sets up a memory model test by initializing buffers and pipeline layouts. */
+ constructor(t: GPUTest, params: MemoryModelTestParams, testShader: string, resultShader: string) {
+ this.test = t;
+ this.params = params;
+
+ // set up buffers
+ const testingThreads = this.params.workgroupSize * this.params.testingWorkgroups;
+ const testLocationsSize =
+ testingThreads * numMemLocations * this.params.memStride * bytesPerWord;
+ const testLocationsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: testLocationsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.makeBufferWithContents(
+ new Uint32Array(testLocationsSize).fill(0),
+ GPUBufferUsage.COPY_SRC
+ ),
+ size: testLocationsSize,
+ };
+
+ const readResultsSize = testingThreads * numReadOutputs * bytesPerWord;
+ const readResultsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: readResultsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.makeBufferWithContents(
+ new Uint32Array(readResultsSize).fill(0),
+ GPUBufferUsage.COPY_SRC
+ ),
+ size: readResultsSize,
+ };
+
+ const testResultsSize = this.params.numBehaviors * bytesPerWord;
+ const testResultsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: testResultsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ }),
+ srcBuf: this.test.makeBufferWithContents(
+ new Uint32Array(testResultsSize).fill(0),
+ GPUBufferUsage.COPY_SRC
+ ),
+ size: testResultsSize,
+ };
+
+ const shuffledWorkgroupsSize = this.params.maxWorkgroups * bytesPerWord;
+ const shuffledWorkgroupsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: shuffledWorkgroupsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.device.createBuffer({
+ size: shuffledWorkgroupsSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ }),
+ size: shuffledWorkgroupsSize,
+ };
+
+ const barrierSize = bytesPerWord;
+ const barrierBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: barrierSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.makeBufferWithContents(
+ new Uint32Array(barrierSize).fill(0),
+ GPUBufferUsage.COPY_SRC
+ ),
+ size: barrierSize,
+ };
+
+ const scratchpadSize = this.params.scratchMemorySize * bytesPerWord;
+ const scratchpadBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: scratchpadSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.makeBufferWithContents(
+ new Uint32Array(scratchpadSize).fill(0),
+ GPUBufferUsage.COPY_SRC
+ ),
+ size: scratchpadSize,
+ };
+
+ const scratchMemoryLocationsSize = this.params.maxWorkgroups * bytesPerWord;
+ const scratchMemoryLocationsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: scratchMemoryLocationsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
+ }),
+ srcBuf: this.test.device.createBuffer({
+ size: scratchMemoryLocationsSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ }),
+ size: scratchMemoryLocationsSize,
+ };
+
+ const stressParamsSize = numStressParams * bytesPerWord;
+ const stressParamsBuffer: BufferWithSource = {
+ deviceBuf: this.test.device.createBuffer({
+ size: stressParamsSize,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
+ }),
+ srcBuf: this.test.device.createBuffer({
+ size: stressParamsSize,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
+ }),
+ size: stressParamsSize,
+ };
+
+ this.buffers = {
+ testLocations: testLocationsBuffer,
+ readResults: readResultsBuffer,
+ testResults: testResultsBuffer,
+ shuffledWorkgroups: shuffledWorkgroupsBuffer,
+ barrier: barrierBuffer,
+ scratchpad: scratchpadBuffer,
+ scratchMemoryLocations: scratchMemoryLocationsBuffer,
+ stressParams: stressParamsBuffer,
+ };
+
+ // set up pipeline layouts
+ const testLayout = this.test.device.createBindGroupLayout({
+ entries: [
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
+ ],
+ });
+ this.testPipeline = this.test.device.createComputePipeline({
+ layout: this.test.device.createPipelineLayout({
+ bindGroupLayouts: [testLayout],
+ }),
+ compute: {
+ module: this.test.device.createShaderModule({
+ code: testShader,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ this.testBindGroup = this.test.device.createBindGroup({
+ entries: [
+ { binding: 0, resource: { buffer: this.buffers.testLocations.deviceBuf } },
+ { binding: 1, resource: { buffer: this.buffers.readResults.deviceBuf } },
+ { binding: 2, resource: { buffer: this.buffers.shuffledWorkgroups.deviceBuf } },
+ { binding: 3, resource: { buffer: this.buffers.barrier.deviceBuf } },
+ { binding: 4, resource: { buffer: this.buffers.scratchpad.deviceBuf } },
+ { binding: 5, resource: { buffer: this.buffers.scratchMemoryLocations.deviceBuf } },
+ { binding: 6, resource: { buffer: this.buffers.stressParams.deviceBuf } },
+ ],
+ layout: testLayout,
+ });
+
+ const resultLayout = this.test.device.createBindGroupLayout({
+ entries: [
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
+ ],
+ });
+ this.resultPipeline = this.test.device.createComputePipeline({
+ layout: this.test.device.createPipelineLayout({
+ bindGroupLayouts: [resultLayout],
+ }),
+ compute: {
+ module: this.test.device.createShaderModule({
+ code: resultShader,
+ }),
+ entryPoint: 'main',
+ },
+ });
+ this.resultBindGroup = this.test.device.createBindGroup({
+ entries: [
+ { binding: 0, resource: { buffer: this.buffers.testLocations.deviceBuf } },
+ { binding: 1, resource: { buffer: this.buffers.readResults.deviceBuf } },
+ { binding: 2, resource: { buffer: this.buffers.testResults.deviceBuf } },
+ { binding: 3, resource: { buffer: this.buffers.stressParams.deviceBuf } },
+ ],
+ layout: resultLayout,
+ });
+ }
+
+ /**
+ * Run the test for the specified number of iterations. Checks the testResults buffer on the weakIndex; if
+ * this value is not 0 then the test has failed. The number of iterations is chosen per test so that the
+ * full set of tests meets some time budget while still being reasonably effective at uncovering issues.
+ * Currently, we aim for each test to complete in under one second.
+ */
+ async run(iterations: number, weakIndex: number): Promise<void> {
+ for (let i = 0; i < iterations; i++) {
+ const numWorkgroups = this.getRandomInRange(
+ this.params.testingWorkgroups,
+ this.params.maxWorkgroups
+ );
+ await this.setShuffledWorkgroups(numWorkgroups);
+ await this.setScratchLocations(numWorkgroups);
+ await this.setStressParams();
+ const encoder = this.test.device.createCommandEncoder();
+ this.copyBufferToBuffer(encoder, this.buffers.testLocations);
+ this.copyBufferToBuffer(encoder, this.buffers.readResults);
+ this.copyBufferToBuffer(encoder, this.buffers.testResults);
+ this.copyBufferToBuffer(encoder, this.buffers.barrier);
+ this.copyBufferToBuffer(encoder, this.buffers.shuffledWorkgroups);
+ this.copyBufferToBuffer(encoder, this.buffers.scratchpad);
+ this.copyBufferToBuffer(encoder, this.buffers.scratchMemoryLocations);
+ this.copyBufferToBuffer(encoder, this.buffers.stressParams);
+
+ const testPass = encoder.beginComputePass();
+ testPass.setPipeline(this.testPipeline);
+ testPass.setBindGroup(0, this.testBindGroup);
+ testPass.dispatchWorkgroups(numWorkgroups);
+ testPass.end();
+
+ const resultPass = encoder.beginComputePass();
+ resultPass.setPipeline(this.resultPipeline);
+ resultPass.setBindGroup(0, this.resultBindGroup);
+ resultPass.dispatchWorkgroups(this.params.testingWorkgroups);
+ resultPass.end();
+
+ this.test.device.queue.submit([encoder.finish()]);
+ this.test.expectGPUBufferValuesPassCheck(
+ this.buffers.testResults.deviceBuf,
+ this.checkWeakIndex(weakIndex),
+ {
+ type: Uint32Array,
+ typedLength: this.params.numBehaviors,
+ }
+ );
+ }
+ }
+
+ /** Returns a function that checks whether the test passes, given a weak index and the test results buffer. */
+ protected checkWeakIndex(weakIndex: number): (a: Uint32Array) => Error | undefined {
+ const checkResult = this.checkResult(weakIndex);
+ const resultPrinter = this.resultPrinter(weakIndex);
+ return function (a: Uint32Array): Error | undefined {
+ return checkElementsPassPredicate(a, checkResult, {
+ predicatePrinter: [{ leftHeader: 'expected ==', getValueForCell: resultPrinter }],
+ });
+ };
+ }
+
+ /**
+ * Returns a function that checks whether the specified weak index's value is not equal to 0.
+ * If the weak index's value is not 0, it means the test has observed a behavior disallowed by the memory model and
+ * is considered a test failure.
+ */
+ protected checkResult(weakIndex: number): (i: number, v: number) => boolean {
+ return function (i: number, v: number): boolean {
+ if (i === weakIndex && v > 0) {
+ return false;
+ }
+ return true;
+ };
+ }
+
+ /** Returns a printer function that visualizes the results of checking the test results. */
+ protected resultPrinter(weakIndex: number): (i: number) => string | number {
+ return function (i: number): string | number {
+ if (i === weakIndex) {
+ return 0;
+ } else {
+ return 'any value';
+ }
+ };
+ }
+
+ /** Utility method that simplifies copying source buffers to device buffers. */
+ protected copyBufferToBuffer(encoder: GPUCommandEncoder, buffer: BufferWithSource): void {
+ encoder.copyBufferToBuffer(buffer.srcBuf, 0, buffer.deviceBuf, 0, buffer.size);
+ }
+
+ /** Returns a random integer between 0 and the max. */
+ protected getRandomInt(max: number): number {
+ return Math.floor(Math.random() * max);
+ }
+
+ /** Returns a random number in between the min and max values. */
+ protected getRandomInRange(min: number, max: number): number {
+ if (min === max) {
+ return min;
+ } else {
+ const offset = this.getRandomInt(max - min);
+ return min + offset;
+ }
+ }
+
+ /** Returns a permuted array using a simple Fisher-Yates shuffle algorithm. */
+ protected shuffleArray(a: number[]): void {
+ for (let i = a.length - 1; i >= 0; i--) {
+ const toSwap = this.getRandomInt(i + 1);
+ const temp = a[toSwap];
+ a[toSwap] = a[i];
+ a[i] = temp;
+ }
+ }
+
+ /**
+ * Shuffles the order of workgroup ids, so that threads operating on the same memory location are not always in
+ * consecutive workgroups.
+ */
+ protected async setShuffledWorkgroups(numWorkgroups: number): Promise<void> {
+ await this.buffers.shuffledWorkgroups.srcBuf.mapAsync(GPUMapMode.WRITE);
+ const shuffledWorkgroupsBuffer = this.buffers.shuffledWorkgroups.srcBuf.getMappedRange();
+ const shuffledWorkgroupsArray = new Uint32Array(shuffledWorkgroupsBuffer);
+ for (let i = 0; i < numWorkgroups; i++) {
+ shuffledWorkgroupsArray[i] = i;
+ }
+ if (this.getRandomInt(100) < this.params.shufflePct) {
+ for (let i = numWorkgroups - 1; i > 0; i--) {
+ const x = this.getRandomInt(i + 1);
+ const temp = shuffledWorkgroupsArray[i];
+ shuffledWorkgroupsArray[i] = shuffledWorkgroupsArray[x];
+ shuffledWorkgroupsArray[x] = temp;
+ }
+ }
+ this.buffers.shuffledWorkgroups.srcBuf.unmap();
+ }
+
+ /** Sets the memory locations that stressing workgroups will access. Uses either a chunking or round robin assignment strategy. */
+ protected async setScratchLocations(numWorkgroups: number): Promise<void> {
+ await this.buffers.scratchMemoryLocations.srcBuf.mapAsync(GPUMapMode.WRITE);
+ const scratchLocationsArrayBuffer = this.buffers.scratchMemoryLocations.srcBuf.getMappedRange();
+ const scratchLocationsArray = new Uint32Array(scratchLocationsArrayBuffer);
+ const scratchNumRegions = this.params.scratchMemorySize / this.params.stressLineSize;
+ const scratchRegions = [...Array(scratchNumRegions).keys()];
+ this.shuffleArray(scratchRegions);
+ for (let i = 0; i < this.params.stressTargetLines; i++) {
+ const region = scratchRegions[i];
+ const locInRegion = this.getRandomInt(this.params.stressLineSize);
+ if (this.getRandomInt(100) < this.params.stressStrategyBalancePct) {
+ // In the round-robin case, the current scratch location is striped across all workgroups.
+ for (let j = i; j < numWorkgroups; j += this.params.stressTargetLines) {
+ scratchLocationsArray[j] = region * this.params.stressLineSize + locInRegion;
+ }
+ } else {
+ // In the chunking case, the current scratch location is assigned to a block of workgroups. The final scratch
+ // location may be assigned to more workgroups, if the number of scratch locations does not cleanly divide the
+ // number of workgroups.
+ const workgroupsPerLocation = numWorkgroups / this.params.stressTargetLines;
+ for (let j = 0; j < workgroupsPerLocation; j++) {
+ scratchLocationsArray[i * workgroupsPerLocation + j] =
+ region * this.params.stressLineSize + locInRegion;
+ }
+ if (
+ i === this.params.stressTargetLines - 1 &&
+ numWorkgroups % this.params.stressTargetLines !== 0
+ ) {
+ for (let j = 0; j < numWorkgroups % this.params.stressTargetLines; j++) {
+ scratchLocationsArray[numWorkgroups - j - 1] =
+ region * this.params.stressLineSize + locInRegion;
+ }
+ }
+ }
+ }
+ this.buffers.scratchMemoryLocations.srcBuf.unmap();
+ }
+
+ /** Sets the parameters that are used by the shader to calculate memory locations and perform stress. */
+ protected async setStressParams(): Promise<void> {
+ await this.buffers.stressParams.srcBuf.mapAsync(GPUMapMode.WRITE);
+ const stressParamsArrayBuffer = this.buffers.stressParams.srcBuf.getMappedRange();
+ const stressParamsArray = new Uint32Array(stressParamsArrayBuffer);
+ if (this.getRandomInt(100) < this.params.barrierPct) {
+ stressParamsArray[barrierParamIndex] = 1;
+ } else {
+ stressParamsArray[barrierParamIndex] = 0;
+ }
+ if (this.getRandomInt(100) < this.params.memStressPct) {
+ stressParamsArray[memStressIndex] = 1;
+ } else {
+ stressParamsArray[memStressIndex] = 0;
+ }
+ stressParamsArray[memStressIterationsIndex] = this.params.memStressIterations;
+ const memStressStoreFirst = this.getRandomInt(100) < this.params.memStressStoreFirstPct;
+ const memStressStoreSecond = this.getRandomInt(100) < this.params.memStressStoreSecondPct;
+ let memStressPattern;
+ if (memStressStoreFirst && memStressStoreSecond) {
+ memStressPattern = 0;
+ } else if (memStressStoreFirst && !memStressStoreSecond) {
+ memStressPattern = 1;
+ } else if (!memStressStoreFirst && memStressStoreSecond) {
+ memStressPattern = 2;
+ } else {
+ memStressPattern = 3;
+ }
+ stressParamsArray[memStressPatternIndex] = memStressPattern;
+ if (this.getRandomInt(100) < this.params.preStressPct) {
+ stressParamsArray[preStressIndex] = 1;
+ } else {
+ stressParamsArray[preStressIndex] = 0;
+ }
+ stressParamsArray[preStressIterationsIndex] = this.params.preStressIterations;
+ const preStressStoreFirst = this.getRandomInt(100) < this.params.preStressStoreFirstPct;
+ const preStressStoreSecond = this.getRandomInt(100) < this.params.preStressStoreSecondPct;
+ let preStressPattern;
+ if (preStressStoreFirst && preStressStoreSecond) {
+ preStressPattern = 0;
+ } else if (preStressStoreFirst && !preStressStoreSecond) {
+ preStressPattern = 1;
+ } else if (!preStressStoreFirst && preStressStoreSecond) {
+ preStressPattern = 2;
+ } else {
+ preStressPattern = 3;
+ }
+ stressParamsArray[preStressPatternIndex] = preStressPattern;
+ stressParamsArray[permuteFirstIndex] = this.params.permuteFirst;
+ stressParamsArray[permuteSecondIndex] = this.params.permuteSecond;
+ stressParamsArray[testingWorkgroupsIndex] = this.params.testingWorkgroups;
+ stressParamsArray[memStrideIndex] = this.params.memStride;
+ if (this.params.aliasedMemory) {
+ stressParamsArray[memLocationOffsetIndex] = 0;
+ } else {
+ stressParamsArray[memLocationOffsetIndex] = this.params.memStride;
+ }
+ this.buffers.stressParams.srcBuf.unmap();
+ }
+}
+
+/** Defines common data structures used in memory model test shaders. */
+const shaderMemStructures = `
+ struct Memory {
+ value: array<u32>
+ };
+
+ struct AtomicMemory {
+ value: array<atomic<u32>>
+ };
+
+ struct ReadResult {
+ r0: atomic<u32>,
+ r1: atomic<u32>,
+ };
+
+ struct ReadResults {
+ value: array<ReadResult>
+ };
+
+ struct StressParamsMemory {
+ do_barrier: u32,
+ mem_stress: u32,
+ mem_stress_iterations: u32,
+ mem_stress_pattern: u32,
+ pre_stress: u32,
+ pre_stress_iterations: u32,
+ pre_stress_pattern: u32,
+ permute_first: u32,
+ permute_second: u32,
+ testing_workgroups: u32,
+ mem_stride: u32,
+ location_offset: u32,
+ };
+`;
+
+/**
+ * Structure to hold the counts of occurrences of the possible behaviors of a two-thread, four-instruction test.
+ * "seq0" means the first invocation's instructions are observed to have occurred before the second invocation's instructions.
+ * "seq1" means the second invocation's instructions are observed to have occurred before the first invocation's instructions.
+ * "interleaved" means there was an observation of some interleaving of instructions between the two invocations.
+ * "weak" means there was an observation of some ordering of instructions that is inconsistent with the WebGPU memory model.
+ */
+const fourBehaviorTestResultStructure = `
+ struct TestResults {
+ seq0: atomic<u32>,
+ seq1: atomic<u32>,
+ interleaved: atomic<u32>,
+ weak: atomic<u32>,
+ };
+`;
+
+/**
+ * Defines the possible behaviors of a two instruction test. Used to test the behavior of non-atomic memory with barriers and
+ * one-thread coherence tests.
+ * "seq" means that the expected, sequential behavior occurred.
+ * "weak" means that an unexpected, inconsistent behavior occurred.
+ */
+const twoBehaviorTestResultStructure = `
+ struct TestResults {
+ seq: atomic<u32>,
+ weak: atomic<u32>,
+ };
+`;
+
+/** Common bindings used in the test shader phase of a test. */
+const commonTestShaderBindings = `
+ @group(0) @binding(1) var<storage, read_write> results : ReadResults;
+ @group(0) @binding(2) var<storage, read> shuffled_workgroups : Memory;
+ @group(0) @binding(3) var<storage, read_write> barrier : AtomicMemory;
+ @group(0) @binding(4) var<storage, read_write> scratchpad : Memory;
+ @group(0) @binding(5) var<storage, read_write> scratch_locations : Memory;
+ @group(0) @binding(6) var<uniform> stress_params : StressParamsMemory;
+`;
+
+/** The combined bindings for a test on atomic memory. */
+const atomicTestShaderBindings = [
+ `
+ @group(0) @binding(0) var<storage, read_write> test_locations : AtomicMemory;
+`,
+ commonTestShaderBindings,
+].join('\n');
+
+/** The combined bindings for a test on non-atomic memory. */
+const nonAtomicTestShaderBindings = [
+ `
+ @group(0) @binding(0) var<storage, read_write> test_locations : Memory;
+`,
+ commonTestShaderBindings,
+].join('\n');
+
+/** Bindings used in the result aggregation phase of the test. */
+const resultShaderBindings = `
+ @group(0) @binding(0) var<storage, read_write> test_locations : AtomicMemory;
+ @group(0) @binding(1) var<storage, read_write> read_results : ReadResults;
+ @group(0) @binding(2) var<storage, read_write> test_results : TestResults;
+ @group(0) @binding(3) var<uniform> stress_params : StressParamsMemory;
+`;
+
+/**
+ * For tests that operate on workgroup memory, include this definition. 3584 memory locations is
+ * large enough to accommodate the maximum memory size needed per workgroup for testing, which is
+ * 256 invocations per workgroup x 2 memory locations x 7 (memStride, or max stride between successive memory locations).
+ * Should change to a pipeline overridable constant when possible.
+ */
+const atomicWorkgroupMemory = `
+ var<workgroup> wg_test_locations: array<atomic<u32>, 3584>;
+`;
+
+/**
+ * For tests that operate on non-atomic workgroup memory, include this definition. 3584 memory locations
+ * is large enough to accommodate the maximum memory size needed per workgroup for testing.
+ */
+const nonAtomicWorkgroupMemory = `
+ var<workgroup> wg_test_locations: array<u32, 3584>;
+`;
+
+/**
+ * Functions used to calculate memory locations for each invocation, for both testing and result aggregation.
+ * The permute function ensures a random permutation based on multiplying and modding by coprime numbers. The stripe
+ * workgroup function ensures that invocations coordinating on a test are spread out across different workgroups.
+ */
+const memoryLocationFunctions = `
+ fn permute_id(id: u32, factor: u32, mask: u32) -> u32 {
+ return (id * factor) % mask;
+ }
+
+ fn stripe_workgroup(workgroup_id: u32, local_id: u32) -> u32 {
+ return (workgroup_id + 1u + local_id % (stress_params.testing_workgroups - 1u)) % stress_params.testing_workgroups;
+ }
+`;
+
+/** Functions that help add stress to the test. */
+const testShaderFunctions = `
+ //Force the invocations in the workgroup to wait for each other, but without the general memory ordering
+ // effects of a control barrier. The barrier spins until either all invocations have incremented the atomic
+ // variable or 1024 loops have occurred. 1024 was chosen because it gives more time for invocations to enter
+ // the barrier but does not overly reduce testing throughput.
+ fn spin(limit: u32) {
+ var i : u32 = 0u;
+ var bar_val : u32 = atomicAdd(&barrier.value[0], 1u);
+ loop {
+ if (i == 1024u || bar_val >= limit) {
+ break;
+ }
+ bar_val = atomicAdd(&barrier.value[0], 0u);
+ i = i + 1u;
+ }
+ }
+
+ // Perform iterations of stress, depending on the specified pattern. Pattern 0 is store-store, pattern 1 is store-load,
+ // pattern 2 is load-store, and pattern 3 is load-load. The extra if condition (if tmpX > 100000u), is used to avoid
+ // the compiler optimizing out unused loads, where 100,000 is larger than the maximum number of stress iterations used
+ // in any test.
+ fn do_stress(iterations: u32, pattern: u32, workgroup_id: u32) {
+ let addr = scratch_locations.value[workgroup_id];
+ switch(pattern) {
+ case 0u: {
+ for(var i: u32 = 0u; i < iterations; i = i + 1u) {
+ scratchpad.value[addr] = i;
+ scratchpad.value[addr] = i + 1u;
+ }
+ }
+ case 1u: {
+ for(var i: u32 = 0u; i < iterations; i = i + 1u) {
+ scratchpad.value[addr] = i;
+ let tmp1: u32 = scratchpad.value[addr];
+ if (tmp1 > 100000u) {
+ scratchpad.value[addr] = i;
+ break;
+ }
+ }
+ }
+ case 2u: {
+ for(var i: u32 = 0u; i < iterations; i = i + 1u) {
+ let tmp1: u32 = scratchpad.value[addr];
+ if (tmp1 > 100000u) {
+ scratchpad.value[addr] = i;
+ break;
+ }
+ scratchpad.value[addr] = i;
+ }
+ }
+ case 3u: {
+ for(var i: u32 = 0u; i < iterations; i = i + 1u) {
+ let tmp1: u32 = scratchpad.value[addr];
+ if (tmp1 > 100000u) {
+ scratchpad.value[addr] = i;
+ break;
+ }
+ let tmp2: u32 = scratchpad.value[addr];
+ if (tmp2 > 100000u) {
+ scratchpad.value[addr] = i;
+ break;
+ }
+ }
+ }
+ default: {
+ }
+ }
+ }
+`;
+
+/**
+ * Entry point to both test and result shaders. One-dimensional workgroup size is hardcoded to 256, until
+ * pipeline overridable constants are supported.
+ */
+const shaderEntryPoint = `
+ // Change to pipeline overridable constant when possible.
+ const workgroupXSize = 256u;
+ @compute @workgroup_size(workgroupXSize) fn main(
+ @builtin(local_invocation_id) local_invocation_id : vec3<u32>,
+ @builtin(workgroup_id) workgroup_id : vec3<u32>) {
+`;
+
+/** All test shaders first calculate the shuffled workgroup. */
+const testShaderCommonHeader = `
+ let shuffled_workgroup = shuffled_workgroups.value[workgroup_id[0]];
+ if (shuffled_workgroup < stress_params.testing_workgroups) {
+`;
+
+/**
+ * All test shaders must calculate addresses for memory locations used in the test. Not all these addresses are
+ * used in every test, but no test uses more than these addresses.
+ */
+const testShaderCommonCalculations = `
+ let x_0 = id_0 * stress_params.mem_stride * 2u;
+ let y_0 = permute_id(id_0, stress_params.permute_second, total_ids) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ let x_1 = id_1 * stress_params.mem_stride * 2u;
+ let y_1 = permute_id(id_1, stress_params.permute_second, total_ids) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ if (stress_params.pre_stress == 1u) {
+ do_stress(stress_params.pre_stress_iterations, stress_params.pre_stress_pattern, shuffled_workgroup);
+ }
+`;
+
+/**
+ * An inter-workgroup test calculates two sets of memory locations that are guaranteed to be in separate workgroups.
+ * If the bounded spin-loop barrier is called, it attempts to wait for all invocations in all workgroups.
+ */
+const interWorkgroupTestShaderCode = [
+ `
+ let total_ids = workgroupXSize * stress_params.testing_workgroups;
+ let id_0 = shuffled_workgroup * workgroupXSize + local_invocation_id[0];
+ let new_workgroup = stripe_workgroup(shuffled_workgroup, local_invocation_id[0]);
+ let id_1 = new_workgroup * workgroupXSize + permute_id(local_invocation_id[0], stress_params.permute_first, workgroupXSize);
+`,
+ testShaderCommonCalculations,
+ `
+ if (stress_params.do_barrier == 1u) {
+ spin(workgroupXSize * stress_params.testing_workgroups);
+ }
+`,
+].join('\n');
+
+/**
+ * An intra-workgroup test calculates two set of memory locations that are guaranteed to be in the same workgroup.
+ * If the bounded spin-loop barrier is called, it attempts to wait for all invocations in the same workgroup.
+ */
+const intraWorkgroupTestShaderCode = [
+ `
+ let total_ids = workgroupXSize;
+ let id_0 = local_invocation_id[0];
+ let id_1 = permute_id(local_invocation_id[0], stress_params.permute_first, workgroupXSize);
+`,
+ testShaderCommonCalculations,
+ `
+ if (stress_params.do_barrier == 1u) {
+ spin(workgroupXSize);
+ }
+`,
+].join('\n');
+
+/**
+ * Tests that operate on storage memory and communicate with invocations in the same workgroup must offset their locations
+ * relative to global memory.
+ */
+const storageIntraWorkgroupTestShaderCode = `
+ let total_ids = workgroupXSize;
+ let id_0 = local_invocation_id[0];
+ let id_1 = permute_id(local_invocation_id[0], stress_params.permute_first, workgroupXSize);
+ let x_0 = (shuffled_workgroup * workgroupXSize + id_0) * stress_params.mem_stride * 2u;
+ let y_0 = (shuffled_workgroup * workgroupXSize + permute_id(id_0, stress_params.permute_second, total_ids)) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ let x_1 = (shuffled_workgroup * workgroupXSize + id_1) * stress_params.mem_stride * 2u;
+ let y_1 = (shuffled_workgroup * workgroupXSize + permute_id(id_1, stress_params.permute_second, total_ids)) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ if (stress_params.pre_stress == 1u) {
+ do_stress(stress_params.pre_stress_iterations, stress_params.pre_stress_pattern, shuffled_workgroup);
+ }
+ if (stress_params.do_barrier == 1u) {
+ spin(workgroupXSize);
+ }
+`;
+
+/** All test shaders may perform stress with non-testing threads. */
+const testShaderCommonFooter = `
+ } else if (stress_params.mem_stress == 1u) {
+ do_stress(stress_params.mem_stress_iterations, stress_params.mem_stress_pattern, shuffled_workgroup);
+ }
+ }
+`;
+
+/**
+ * All result shaders must calculate memory locations used in the test. Not all these locations are
+ * used in every result shader, but no result shader uses more than these locations.
+ */
+const resultShaderCommonCalculations = `
+ let id_0 = workgroup_id[0] * workgroupXSize + local_invocation_id[0];
+ let x_0 = id_0 * stress_params.mem_stride * 2u;
+ let mem_x_0 = atomicLoad(&test_locations.value[x_0]);
+ let r0 = atomicLoad(&read_results.value[id_0].r0);
+ let r1 = atomicLoad(&read_results.value[id_0].r1);
+`;
+
+/** Common result shader code for an inter-workgroup test. */
+const interWorkgroupResultShaderCode = [
+ resultShaderCommonCalculations,
+ `
+ let total_ids = workgroupXSize * stress_params.testing_workgroups;
+ let y_0 = permute_id(id_0, stress_params.permute_second, total_ids) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ let mem_y_0 = atomicLoad(&test_locations.value[y_0]);
+`,
+].join('\n');
+
+/** Common result shader code for an intra-workgroup test. */
+const intraWorkgroupResultShaderCode = [
+ resultShaderCommonCalculations,
+ `
+ let total_ids = workgroupXSize;
+ let y_0 = (workgroup_id[0] * workgroupXSize + permute_id(local_invocation_id[0], stress_params.permute_second, total_ids)) * stress_params.mem_stride * 2u + stress_params.location_offset;
+ let mem_y_0 = atomicLoad(&test_locations.value[y_0]);
+`,
+].join('\n');
+
+/** Ending bracket for result shaders. */
+const resultShaderCommonFooter = `
+}
+`;
+
+/** The common shader code for test shaders that perform atomic storage class memory litmus tests. */
+const storageMemoryAtomicTestShaderCode = [
+ shaderMemStructures,
+ atomicTestShaderBindings,
+ memoryLocationFunctions,
+ testShaderFunctions,
+ shaderEntryPoint,
+ testShaderCommonHeader,
+].join('\n');
+
+/** The common shader code for test shaders that perform non-atomic storage class memory litmus tests. */
+const storageMemoryNonAtomicTestShaderCode = [
+ shaderMemStructures,
+ nonAtomicTestShaderBindings,
+ memoryLocationFunctions,
+ testShaderFunctions,
+ shaderEntryPoint,
+ testShaderCommonHeader,
+].join('\n');
+
+/** The common shader code for test shaders that perform atomic workgroup class memory litmus tests. */
+const workgroupMemoryAtomicTestShaderCode = [
+ shaderMemStructures,
+ atomicTestShaderBindings,
+ atomicWorkgroupMemory,
+ memoryLocationFunctions,
+ testShaderFunctions,
+ shaderEntryPoint,
+ testShaderCommonHeader,
+].join('\n');
+
+/** The common shader code for test shaders that perform non-atomic workgroup class memory litmus tests. */
+const workgroupMemoryNonAtomicTestShaderCode = [
+ shaderMemStructures,
+ nonAtomicTestShaderBindings,
+ nonAtomicWorkgroupMemory,
+ memoryLocationFunctions,
+ testShaderFunctions,
+ shaderEntryPoint,
+ testShaderCommonHeader,
+].join('\n');
+
+/** The common shader code for all result shaders. */
+const resultShaderCommonCode = [
+ shaderMemStructures,
+ resultShaderBindings,
+ memoryLocationFunctions,
+ shaderEntryPoint,
+].join('\n');
+
+/**
+ * Defines the types of possible memory a test is operating on. Used as part of the process of building shader code from
+ * its composite parts.
+ */
+export enum MemoryType {
+ /** Atomic memory in the storage address space. */
+ AtomicStorageClass = 'atomic_storage',
+ /** Non-atomic memory in the storage address space. */
+ NonAtomicStorageClass = 'non_atomic_storage',
+ /** Atomic memory in the workgroup address space. */
+ AtomicWorkgroupClass = 'atomic_workgroup',
+ /** Non-atomic memory in the workgroup address space. */
+ NonAtomicWorkgroupClass = 'non_atomic_workgroup',
+}
+
+/**
+ * Defines the relative positions of two invocations coordinating on a test. Used as part of the process of building shader
+ * code from its composite parts.
+ */
+export enum TestType {
+ /** A test consists of two invocations in different workgroups. */
+ InterWorkgroup = 'inter_workgroup',
+ /** A test consists of two invocations in the same workgroup. */
+ IntraWorkgroup = 'intra_workgroup',
+}
+
+/** Defines the number of behaviors a test may have. */
+export enum ResultType {
+ TwoBehavior,
+ FourBehavior,
+}
+
+/**
+ * Given test code that performs the actual sequence of loads and stores, as well as a memory type and test type, returns
+ * a complete test shader.
+ */
+export function buildTestShader(
+ testCode: string,
+ memoryType: MemoryType,
+ testType: TestType
+): string {
+ let memoryTypeCode;
+ let isStorageAS = false;
+ switch (memoryType) {
+ case MemoryType.AtomicStorageClass:
+ memoryTypeCode = storageMemoryAtomicTestShaderCode;
+ isStorageAS = true;
+ break;
+ case MemoryType.NonAtomicStorageClass:
+ memoryTypeCode = storageMemoryNonAtomicTestShaderCode;
+ isStorageAS = true;
+ break;
+ case MemoryType.AtomicWorkgroupClass:
+ memoryTypeCode = workgroupMemoryAtomicTestShaderCode;
+ break;
+ case MemoryType.NonAtomicWorkgroupClass:
+ memoryTypeCode = workgroupMemoryNonAtomicTestShaderCode;
+ }
+ let testTypeCode;
+ switch (testType) {
+ case TestType.InterWorkgroup:
+ testTypeCode = interWorkgroupTestShaderCode;
+ break;
+ case TestType.IntraWorkgroup:
+ if (isStorageAS) {
+ testTypeCode = storageIntraWorkgroupTestShaderCode;
+ } else {
+ testTypeCode = intraWorkgroupTestShaderCode;
+ }
+ }
+ return [memoryTypeCode, testTypeCode, testCode, testShaderCommonFooter].join('\n');
+}
+
+/**
+ * Given result code that aggregates the possible behaviors of a test across all instances, as well as a test type and
+ * number of behaviors, returns a complete result shader.
+ */
+export function buildResultShader(
+ resultCode: string,
+ testType: TestType,
+ resultType: ResultType
+): string {
+ let resultStructure;
+ switch (resultType) {
+ case ResultType.TwoBehavior:
+ resultStructure = twoBehaviorTestResultStructure;
+ break;
+ case ResultType.FourBehavior:
+ resultStructure = fourBehaviorTestResultStructure;
+ }
+ let testTypeCode;
+ switch (testType) {
+ case TestType.InterWorkgroup:
+ testTypeCode = interWorkgroupResultShaderCode;
+ break;
+ case TestType.IntraWorkgroup:
+ testTypeCode = intraWorkgroupResultShaderCode;
+ }
+ return [
+ resultStructure,
+ resultShaderCommonCode,
+ testTypeCode,
+ resultCode,
+ resultShaderCommonFooter,
+ ].join('\n');
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/weak.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/weak.spec.ts
new file mode 100644
index 0000000000..68f86a7d00
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/memory_model/weak.spec.ts
@@ -0,0 +1,429 @@
+export const description = `
+Tests for properties of the WebGPU memory model involving two memory locations.
+Specifically, the acquire/release ordering provided by WebGPU's barriers can be used to disallow
+weak behaviors in several classic memory model litmus tests.`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+import {
+ MemoryModelTestParams,
+ MemoryModelTester,
+ buildTestShader,
+ MemoryType,
+ TestType,
+ buildResultShader,
+ ResultType,
+} from './memory_model_setup.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// A reasonable parameter set, determined heuristically.
+const memoryModelTestParams: MemoryModelTestParams = {
+ workgroupSize: 256,
+ testingWorkgroups: 739,
+ maxWorkgroups: 885,
+ shufflePct: 0,
+ barrierPct: 0,
+ memStressPct: 0,
+ memStressIterations: 1024,
+ memStressStoreFirstPct: 50,
+ memStressStoreSecondPct: 50,
+ preStressPct: 100,
+ preStressIterations: 33,
+ preStressStoreFirstPct: 0,
+ preStressStoreSecondPct: 100,
+ scratchMemorySize: 1408,
+ stressLineSize: 4,
+ stressTargetLines: 11,
+ stressStrategyBalancePct: 0,
+ permuteFirst: 109,
+ permuteSecond: 419,
+ memStride: 2,
+ aliasedMemory: false,
+ numBehaviors: 4,
+};
+
+const workgroupMemoryMessagePassingTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[y_0], 1u);
+ let r0 = atomicLoad(&wg_test_locations[y_1]);
+ workgroupBarrier();
+ let r1 = atomicLoad(&wg_test_locations[x_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const storageMemoryMessagePassingTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ storageBarrier();
+ atomicStore(&test_locations.value[y_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[y_1]);
+ storageBarrier();
+ let r1 = atomicLoad(&test_locations.value[x_1]);
+ atomicStore(&results.value[shuffled_workgroup * u32(workgroupXSize) + id_1].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * u32(workgroupXSize) + id_1].r1, r1);
+`;
+
+g.test('message_passing')
+ .desc(
+ `Checks whether two reads on one thread can observe two writes in another thread in a way
+ that is inconsistent with sequential consistency. In the message passing litmus test, one
+ thread writes the value 1 to some location x and then 1 to some location y. The second thread
+ reads y and then x. If the second thread reads y == 1 and x == 0, then sequential consistency
+ has not been respected. The acquire/release semantics of WebGPU's barrier functions should disallow
+ this behavior within a workgroup.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemoryMessagePassingTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemoryMessagePassingTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((r0 == 0u && r1 == 0u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 1u && r1 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && r1 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 1u && r1 == 0u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
+
+const workgroupMemoryStoreTestCode = `
+ atomicStore(&wg_test_locations[x_0], 2u);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[y_0], 1u);
+ let r0 = atomicLoad(&wg_test_locations[y_1]);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[x_1], 1u);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+`;
+
+const storageMemoryStoreTestCode = `
+ atomicStore(&test_locations.value[x_0], 2u);
+ storageBarrier();
+ atomicStore(&test_locations.value[y_0], 1u);
+ let r0 = atomicLoad(&test_locations.value[y_1]);
+ storageBarrier();
+ atomicStore(&test_locations.value[x_1], 1u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+`;
+
+g.test('store')
+ .desc(
+ `In the store litmus test, one thread writes 2 to some memory location x and then 1 to some memory location
+ y. A second thread reads the value of y and then writes 1 to x. If the read on the second thread returns 1,
+ but the value of x in memory after the test ends is 2, then there has been a re-ordering which is not allowed
+ when using WebGPU's barriers.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemoryStoreTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemoryStoreTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((r0 == 1u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 0u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && mem_x_0 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 1u && mem_x_0 == 2u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
+
+const workgroupMemoryLoadBufferTestCode = `
+ let r0 = atomicLoad(&wg_test_locations[y_0]);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[x_0], 1u);
+ let r1 = atomicLoad(&wg_test_locations[x_1]);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[y_1], 1u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const storageMemoryLoadBufferTestCode = `
+ let r0 = atomicLoad(&test_locations.value[y_0]);
+ storageBarrier();
+ atomicStore(&test_locations.value[x_0], 1u);
+ let r1 = atomicLoad(&test_locations.value[x_1]);
+ storageBarrier();
+ atomicStore(&test_locations.value[y_1], 1u);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+g.test('load_buffer')
+ .desc(
+ `In the load buffer litmus test, one thread reads from memory location y and then writes 1 to memory location x.
+ A second thread reads from x and then writes 1 to y. If both threads read the value 0, then the loads have been
+ buffered or re-ordered, which is not allowed when used in conjunction with WebGPU's barriers.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemoryLoadBufferTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemoryLoadBufferTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((r0 == 1u && r1 == 0u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 0u && r1 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 0u && r1 == 0u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 1u && r1 == 1u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
+
+const workgroupMemoryReadTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ workgroupBarrier();
+ atomicExchange(&wg_test_locations[y_0], 1u);
+ atomicExchange(&wg_test_locations[y_1], 2u);
+ workgroupBarrier();
+ let r0 = atomicLoad(&wg_test_locations[x_1]);
+ workgroupBarrier();
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + y_1], atomicLoad(&wg_test_locations[y_1]));
+`;
+
+const storageMemoryReadTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ storageBarrier();
+ atomicExchange(&test_locations.value[y_0], 1u);
+ atomicExchange(&test_locations.value[y_1], 2u);
+ storageBarrier();
+ let r0 = atomicLoad(&test_locations.value[x_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r0, r0);
+`;
+
+g.test('read')
+ .desc(
+ `In the read litmus test, one thread writes 1 to memory location x and then 1 to memory location y. A second thread
+ first writes 2 to y and then reads from x. If the value read by the second thread is 0 but the value in memory of y
+ after the test completes is 2, then there has been some re-ordering of instructions disallowed when using WebGPU's
+ barrier. Additionally, both writes to y are RMWs, so that the barrier forces the correct acquire/release memory ordering
+ synchronization.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemoryReadTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemoryReadTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((r0 == 1u && mem_y_0 == 2u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 0u && mem_y_0 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 1u && mem_y_0 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 0u && mem_y_0 == 2u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
+
+const workgroupMemoryStoreBufferTestCode = `
+ atomicStore(&wg_test_locations[x_0], 1u);
+ workgroupBarrier();
+ let r0 = atomicAdd(&wg_test_locations[y_0], 0u);
+ atomicExchange(&wg_test_locations[y_1], 1u);
+ workgroupBarrier();
+ let r1 = atomicLoad(&wg_test_locations[x_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+const storageMemoryStoreBufferTestCode = `
+ atomicStore(&test_locations.value[x_0], 1u);
+ storageBarrier();
+ let r0 = atomicAdd(&test_locations.value[y_0], 0u);
+ atomicExchange(&test_locations.value[y_1], 1u);
+ storageBarrier();
+ let r1 = atomicLoad(&test_locations.value[x_1]);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_0].r0, r0);
+ atomicStore(&results.value[shuffled_workgroup * workgroupXSize + id_1].r1, r1);
+`;
+
+g.test('store_buffer')
+ .desc(
+ `In the store buffer litmus test, one thread writes 1 to memory location x and then reads from memory location
+ y. A second thread writes 1 to y and then reads from x. If both reads return 0, then stores have been buffered
+ or some other re-ordering has occurred that is disallowed by WebGPU's barriers. Additionally, both the read
+ and store to y are RMWs to achieve the necessary synchronization across threads.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemoryStoreBufferTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemoryStoreBufferTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((r0 == 1u && r1 == 0u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((r0 == 0u && r1 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((r0 == 1u && r1 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((r0 == 0u && r1 == 0u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
+
+const workgroupMemory2P2WTestCode = `
+ atomicStore(&wg_test_locations[x_0], 2u);
+ workgroupBarrier();
+ atomicExchange(&wg_test_locations[y_0], 1u);
+ atomicExchange(&wg_test_locations[y_1], 2u);
+ workgroupBarrier();
+ atomicStore(&wg_test_locations[x_1], 1u);
+ workgroupBarrier();
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + x_1], atomicLoad(&wg_test_locations[x_1]));
+ atomicStore(&test_locations.value[shuffled_workgroup * workgroupXSize * stress_params.mem_stride * 2u + y_1], atomicLoad(&wg_test_locations[y_1]));
+`;
+
+const storageMemory2P2WTestCode = `
+ atomicStore(&test_locations.value[x_0], 2u);
+ storageBarrier();
+ atomicExchange(&test_locations.value[y_0], 1u);
+ atomicExchange(&test_locations.value[y_1], 2u);
+ storageBarrier();
+ atomicStore(&test_locations.value[x_1], 1u);
+`;
+
+g.test('2_plus_2_write')
+ .desc(
+ `In the 2+2 write litmus test, one thread stores 2 to memory location x and then 1 to memory location y.
+ A second thread stores 2 to y and then 1 to x. If at the end of the test both memory locations are set to 2,
+ then some disallowed re-ordering has occurred. Both writes to y are RMWs to achieve the required synchronization.
+ `
+ )
+ .paramsSimple([
+ { memType: MemoryType.AtomicWorkgroupClass, _testCode: workgroupMemory2P2WTestCode },
+ { memType: MemoryType.AtomicStorageClass, _testCode: storageMemory2P2WTestCode },
+ ])
+ .fn(async t => {
+ const testShader = buildTestShader(
+ t.params._testCode,
+ t.params.memType,
+ TestType.IntraWorkgroup
+ );
+ const messagePassingResultShader = buildResultShader(
+ `
+ if ((mem_x_0 == 1u && mem_y_0 == 2u)) {
+ atomicAdd(&test_results.seq0, 1u);
+ } else if ((mem_x_0 == 2u && mem_y_0 == 1u)) {
+ atomicAdd(&test_results.seq1, 1u);
+ } else if ((mem_x_0 == 1u && mem_y_0 == 1u)) {
+ atomicAdd(&test_results.interleaved, 1u);
+ } else if ((mem_x_0 == 2u && mem_y_0 == 2u)) {
+ atomicAdd(&test_results.weak, 1u);
+ }
+ `,
+ TestType.IntraWorkgroup,
+ ResultType.FourBehavior
+ );
+ const memModelTester = new MemoryModelTester(
+ t,
+ memoryModelTestParams,
+ testShader,
+ messagePassingResultShader
+ );
+ await memModelTester.run(40, 3);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/padding.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/padding.spec.ts
new file mode 100644
index 0000000000..7bc31a7712
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/padding.spec.ts
@@ -0,0 +1,423 @@
+export const description = `
+Execution Tests for preservation of padding bytes in structures and arrays.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { iterRange } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+/**
+ * Run a shader and check that the buffer output matches expectations.
+ *
+ * @param t The test object
+ * @param wgsl The shader source
+ * @param expected The array of expected values after running the shader
+ */
+function runShaderTest(t: GPUTest, wgsl: string, expected: Uint32Array): void {
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ entryPoint: 'main',
+ },
+ });
+
+ // Allocate a buffer and fill it with 0xdeadbeef words.
+ const outputBuffer = t.makeBufferWithContents(
+ new Uint32Array([...iterRange(expected.length, x => 0xdeadbeef)]),
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
+ );
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: outputBuffer } }],
+ });
+
+ // Run the shader.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // Check that only the non-padding bytes were modified.
+ t.expectGPUBufferValuesEqual(outputBuffer, expected);
+}
+
+g.test('struct_implicit')
+ .desc(
+ `Test that padding bytes in between structure members are preserved.
+
+ This test defines a structure that has implicit padding and creates a read-write storage
+ buffer with that structure type. The shader assigns the whole variable at once, and we
+ then test that data in the padding bytes was preserved.
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ struct S {
+ a : u32,
+ // 12 bytes of padding
+ b : vec3<u32>,
+ // 4 bytes of padding
+ c : vec2<u32>,
+ // 8 bytes of padding
+ }
+ @group(0) @binding(0) var<storage, read_write> buffer : S;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = S(0x12345678, vec3(0xabcdef01), vec2(0x98765432));
+ }
+ `;
+ runShaderTest(
+ t,
+ wgsl,
+ new Uint32Array([
+ // a : u32
+ 0x12345678,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // b : vec3<u32>
+ 0xabcdef01,
+ 0xabcdef01,
+ 0xabcdef01,
+ 0xdeadbeef,
+ // c : vec2<u32>
+ 0x98765432,
+ 0x98765432,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ ])
+ );
+ });
+
+g.test('struct_explicit')
+ .desc(
+ `Test that padding bytes in between structure members are preserved.
+
+ This test defines a structure with explicit padding attributes and creates a read-write storage
+ buffer with that structure type. The shader assigns the whole variable at once, and we
+ then test that data in the padding bytes was preserved.
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ struct S {
+ a : u32,
+ // 12 bytes of padding
+ @align(16) @size(20) b : u32,
+ // 16 bytes of padding
+ @size(12) c : u32,
+ // 8 bytes of padding
+ }
+ @group(0) @binding(0) var<storage, read_write> buffer : S;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = S(0x12345678, 0xabcdef01, 0x98765432);
+ }
+ `;
+ runShaderTest(
+ t,
+ wgsl,
+ new Uint32Array([
+ // a : u32
+ 0x12345678,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // @align(16) @size(20) b : u32
+ 0xabcdef01,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // @size(12) c : u32
+ 0x98765432,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ ])
+ );
+ });
+
+g.test('struct_nested')
+ .desc(
+ `Test that padding bytes in nested structures are preserved.
+
+ This test defines a set of nested structures that have padding and creates a read-write storage
+ buffer with the root structure type. The shader assigns the whole variable at once, and we
+ then test that data in the padding bytes was preserved.
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ // Size of S1 is 48 bytes.
+ // Alignment of S1 is 16 bytes.
+ struct S1 {
+ a : u32,
+ // 12 bytes of padding
+ b : vec3<u32>,
+ // 4 bytes of padding
+ c : vec2<u32>,
+ // 8 bytes of padding
+ }
+
+ // Size of S2 is 112 bytes.
+ // Alignment of S2 is 48 bytes.
+ struct S2 {
+ a2 : u32,
+ // 12 bytes of padding
+ b2 : S1,
+ c2 : S1,
+ }
+
+ // Size of S3 is 144 bytes.
+ // Alignment of S3 is 48 bytes.
+ struct S3 {
+ a3 : S1,
+ b3 : S2,
+ c3 : S2,
+ }
+
+ @group(0) @binding(0) var<storage, read_write> buffer : S3;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = S3();
+ }
+ `;
+ runShaderTest(
+ t,
+ wgsl,
+ new Uint32Array([
+ // a3 : S1
+ // a3.a1 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // a3.b1 : vec3<u32>
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ // a3.c1 : vec2<u32>
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+
+ // b3 : S2
+ // b3.a2 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // b3.b2 : S1
+ // b3.b2.a1 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // b3.b2.b1 : vec3<u32>
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ // b3.b2.c1 : vec2<u32>
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // b3.c2 : S1
+ // b3.c2.a1 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // b3.c2.b1 : vec3<u32>
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ // b3.c2.c1 : vec2<u32>
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+
+ // c3 : S2
+ // c3.a2 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // c3.b2 : S1
+ // c3.b2.a1 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // c3.b2.b1 : vec3<u32>
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ // c3.b2.c1 : vec2<u32>
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // c3.c2 : S1
+ // c3.c2.a1 : u32
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ // c3.c2.b1 : vec3<u32>
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ // c3.c2.c1 : vec2<u32>
+ 0x00000000,
+ 0x00000000,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ ])
+ );
+ });
+
+g.test('array_of_vec3')
+ .desc(
+ `Test that padding bytes in between array elements are preserved.
+
+ This test defines creates a read-write storage buffer with type array<vec3, 4>. The shader
+ assigns the whole variable at once, and we then test that data in the padding bytes was
+ preserved.
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ @group(0) @binding(0) var<storage, read_write> buffer : array<vec3<u32>, 4>;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = array<vec3<u32>, 4>(
+ vec3(0x12345678),
+ vec3(0xabcdef01),
+ vec3(0x98765432),
+ vec3(0x0f0f0f0f),
+ );
+ }
+ `;
+ runShaderTest(
+ t,
+ wgsl,
+ new Uint32Array([
+ // buffer[0]
+ 0x12345678,
+ 0x12345678,
+ 0x12345678,
+ 0xdeadbeef,
+ // buffer[1]
+ 0xabcdef01,
+ 0xabcdef01,
+ 0xabcdef01,
+ 0xdeadbeef,
+ // buffer[2]
+ 0x98765432,
+ 0x98765432,
+ 0x98765432,
+ 0xdeadbeef,
+ // buffer[2]
+ 0x0f0f0f0f,
+ 0x0f0f0f0f,
+ 0x0f0f0f0f,
+ 0xdeadbeef,
+ ])
+ );
+ });
+
+g.test('array_of_struct')
+ .desc(
+ `Test that padding bytes in between array elements are preserved.
+
+ This test defines creates a read-write storage buffer with type array<S, 4>, where S is a
+ structure that contains padding bytes. The shader assigns the whole variable at once, and we
+ then test that data in the padding bytes was preserved.
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ struct S {
+ a : u32,
+ b : vec3<u32>,
+ }
+ @group(0) @binding(0) var<storage, read_write> buffer : array<S, 3>;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = array<S, 3>(
+ S(0x12345678, vec3(0x0f0f0f0f)),
+ S(0xabcdef01, vec3(0x7c7c7c7c)),
+ S(0x98765432, vec3(0x18181818)),
+ );
+ }
+ `;
+ runShaderTest(
+ t,
+ wgsl,
+ new Uint32Array([
+ // buffer[0]
+ 0x12345678,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0x0f0f0f0f,
+ 0x0f0f0f0f,
+ 0x0f0f0f0f,
+ 0xdeadbeef,
+ // buffer[1]
+ 0xabcdef01,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0x7c7c7c7c,
+ 0x7c7c7c7c,
+ 0x7c7c7c7c,
+ 0xdeadbeef,
+ // buffer[2]
+ 0x98765432,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0xdeadbeef,
+ 0x18181818,
+ 0x18181818,
+ 0x18181818,
+ 0xdeadbeef,
+ ])
+ );
+ });
+
+g.test('vec3')
+ .desc(
+ `Test padding bytes are preserved when assigning to a variable of type vec3 (without a struct).
+ `
+ )
+ .fn(async t => {
+ const wgsl = `
+ @group(0) @binding(0) var<storage, read_write> buffer : vec3<u32>;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ buffer = vec3<u32>(0x12345678, 0xabcdef01, 0x98765432);
+ }
+ `;
+ runShaderTest(t, wgsl, new Uint32Array([0x12345678, 0xabcdef01, 0x98765432, 0xdeadbeef]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts
new file mode 100644
index 0000000000..69f92beaab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access.spec.ts
@@ -0,0 +1,480 @@
+export const description = `
+Tests to check datatype clamping in shaders is correctly implemented for all indexable types
+(vectors, matrices, sized/unsized arrays) visible to shaders in various ways.
+
+TODO: add tests to check that textureLoad operations stay in-bounds.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+import { align } from '../../util/math.js';
+import { generateTypes, supportedScalarTypes, supportsAtomics } from '../types.js';
+
+export const g = makeTestGroup(GPUTest);
+
+const kMaxU32 = 0xffff_ffff;
+const kMaxI32 = 0x7fff_ffff;
+const kMinI32 = -0x8000_0000;
+
+/**
+ * Wraps the provided source into a harness that checks calling `runTest()` returns 0.
+ *
+ * Non-test bindings are in bind group 1, including:
+ * - `constants.zero`: a dynamically-uniform `0u` value.
+ */
+function runShaderTest(
+ t: GPUTest,
+ stage: GPUShaderStageFlags,
+ testSource: string,
+ layout: GPUPipelineLayout,
+ testBindings: GPUBindGroupEntry[],
+ dynamicOffsets?: number[]
+): void {
+ assert(stage === GPUShaderStage.COMPUTE, 'Only know how to deal with compute for now');
+
+ // Contains just zero (for now).
+ const constantsBuffer = t.device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM });
+
+ const resultBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE,
+ });
+
+ const source = `
+struct Constants {
+ zero: u32
+};
+@group(1) @binding(0) var<uniform> constants: Constants;
+
+struct Result {
+ value: u32
+};
+@group(1) @binding(1) var<storage, read_write> result: Result;
+
+${testSource}
+
+@compute @workgroup_size(1)
+fn main() {
+ _ = constants.zero; // Ensure constants buffer is statically-accessed
+ result.value = runTest();
+}`;
+
+ t.debug(source);
+ const module = t.device.createShaderModule({ code: source });
+ const pipeline = t.device.createComputePipeline({
+ layout,
+ compute: { module, entryPoint: 'main' },
+ });
+
+ const group = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(1),
+ entries: [
+ { binding: 0, resource: { buffer: constantsBuffer } },
+ { binding: 1, resource: { buffer: resultBuffer } },
+ ],
+ });
+
+ const testGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: testBindings,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, testGroup, dynamicOffsets);
+ pass.setBindGroup(1, group);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+
+ t.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array([0]));
+}
+
+/** Fill an ArrayBuffer with sentinel values, except clear a region to zero. */
+function testFillArrayBuffer(
+ array: ArrayBuffer,
+ type: 'u32' | 'i32' | 'f32',
+ { zeroByteStart, zeroByteCount }: { zeroByteStart: number; zeroByteCount: number }
+) {
+ const constructor = { u32: Uint32Array, i32: Int32Array, f32: Float32Array }[type];
+ assert(zeroByteCount % constructor.BYTES_PER_ELEMENT === 0);
+ new constructor(array).fill(42);
+ new constructor(array, zeroByteStart, zeroByteCount / constructor.BYTES_PER_ELEMENT).fill(0);
+}
+
+/**
+ * Generate a bunch of indexable types (vec, mat, sized/unsized array) for testing.
+ */
+
+g.test('linear_memory')
+ .desc(
+ `For each indexable data type (vec, mat, sized/unsized array, of various scalar types), attempts
+ to access (read, write, atomic load/store) a region of memory (buffer or internal) at various
+ (signed/unsigned) indices. Checks that the accesses conform to robust access (OOB reads only
+ return bound memory, OOB writes don't write OOB).
+
+ TODO: Test in/out storage classes.
+ TODO: Test vertex and fragment stages.
+ TODO: Test using a dynamic offset instead of a static offset into uniform/storage bindings.
+ TODO: Test types like vec2<atomic<i32>>, if that's allowed.
+ TODO: Test exprIndexAddon as constexpr.
+ TODO: Test exprIndexAddon as pipeline-overridable constant expression.
+ `
+ )
+ .params(u =>
+ u
+ .combineWithParams([
+ { storageClass: 'storage', storageMode: 'read', access: 'read', dynamicOffset: false },
+ {
+ storageClass: 'storage',
+ storageMode: 'read_write',
+ access: 'read',
+ dynamicOffset: false,
+ },
+ {
+ storageClass: 'storage',
+ storageMode: 'read_write',
+ access: 'write',
+ dynamicOffset: false,
+ },
+ { storageClass: 'storage', storageMode: 'read', access: 'read', dynamicOffset: true },
+ { storageClass: 'storage', storageMode: 'read_write', access: 'read', dynamicOffset: true },
+ {
+ storageClass: 'storage',
+ storageMode: 'read_write',
+ access: 'write',
+ dynamicOffset: true,
+ },
+ { storageClass: 'uniform', access: 'read', dynamicOffset: false },
+ { storageClass: 'uniform', access: 'read', dynamicOffset: true },
+ { storageClass: 'private', access: 'read' },
+ { storageClass: 'private', access: 'write' },
+ { storageClass: 'function', access: 'read' },
+ { storageClass: 'function', access: 'write' },
+ { storageClass: 'workgroup', access: 'read' },
+ { storageClass: 'workgroup', access: 'write' },
+ ] as const)
+ .combineWithParams([
+ { containerType: 'array' },
+ { containerType: 'matrix' },
+ { containerType: 'vector' },
+ ] as const)
+ .combineWithParams([
+ { shadowingMode: 'none' },
+ { shadowingMode: 'module-scope' },
+ { shadowingMode: 'function-scope' },
+ ])
+ .expand('isAtomic', p => (supportsAtomics(p) ? [false, true] : [false]))
+ .beginSubcases()
+ .expand('baseType', supportedScalarTypes)
+ .expandWithParams(generateTypes)
+ )
+ .fn(async t => {
+ const {
+ storageClass,
+ storageMode,
+ access,
+ dynamicOffset,
+ isAtomic,
+ containerType,
+ baseType,
+ type,
+ shadowingMode,
+ _kTypeInfo,
+ } = t.params;
+
+ assert(_kTypeInfo !== undefined, 'not an indexable type');
+ assert('arrayLength' in _kTypeInfo);
+
+ let usesCanary = false;
+ let globalSource = '';
+ let testFunctionSource = '';
+ const testBufferSize = 512;
+ const bufferBindingOffset = 256;
+ /** Undefined if no buffer binding is needed */
+ let bufferBindingSize: number | undefined = undefined;
+
+ // Declare the data that will be accessed to check robust access, as a buffer or a struct
+ // in the global scope or inside the test function itself.
+ const structDecl = `
+struct S {
+ startCanary: array<u32, 10>,
+ data: ${type},
+ endCanary: array<u32, 10>,
+};`;
+
+ const testGroupBGLEntires: GPUBindGroupLayoutEntry[] = [];
+ switch (storageClass) {
+ case 'uniform':
+ case 'storage':
+ {
+ assert(_kTypeInfo.layout !== undefined);
+ const layout = _kTypeInfo.layout;
+ bufferBindingSize = align(layout.size, layout.alignment);
+ const qualifiers = storageClass === 'storage' ? `storage, ${storageMode}` : storageClass;
+ globalSource += `
+struct TestData {
+ data: ${type},
+};
+@group(0) @binding(0) var<${qualifiers}> s: TestData;`;
+
+ testGroupBGLEntires.push({
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type:
+ storageClass === 'uniform'
+ ? 'uniform'
+ : storageMode === 'read'
+ ? 'read-only-storage'
+ : 'storage',
+ hasDynamicOffset: dynamicOffset,
+ },
+ });
+ }
+ break;
+
+ case 'private':
+ case 'workgroup':
+ usesCanary = true;
+ globalSource += structDecl;
+ globalSource += `var<${storageClass}> s: S;`;
+ break;
+
+ case 'function':
+ usesCanary = true;
+ globalSource += structDecl;
+ testFunctionSource += 'var s: S;';
+ break;
+ }
+
+ // Build the test function that will do the tests.
+
+ // If we use a local canary declared in the shader, initialize it.
+ if (usesCanary) {
+ testFunctionSource += `
+ for (var i = 0u; i < 10u; i = i + 1u) {
+ s.startCanary[i] = 0xFFFFFFFFu;
+ s.endCanary[i] = 0xFFFFFFFFu;
+ }`;
+ }
+
+ /** Returns a different number each time, kind of like a `__LINE__` to ID the failing check. */
+ const nextErrorReturnValue = (() => {
+ let errorReturnValue = 0x1000;
+ return () => {
+ ++errorReturnValue;
+ return `0x${errorReturnValue.toString(16)}u`;
+ };
+ })();
+
+ // This is here, instead of in subcases, so only a single shader is needed to test many modes.
+ for (const indexSigned of [false, true]) {
+ const indicesToTest = indexSigned
+ ? [
+ // Exactly in bounds (should be OK)
+ '0',
+ `${_kTypeInfo.arrayLength} - 1`,
+ // Exactly out of bounds
+ '-1',
+ `${_kTypeInfo.arrayLength}`,
+ // Far out of bounds
+ '-1000000',
+ '1000000',
+ `${kMinI32}`,
+ `${kMaxI32}`,
+ ]
+ : [
+ // Exactly in bounds (should be OK)
+ '0u',
+ `${_kTypeInfo.arrayLength}u - 1u`,
+ // Exactly out of bounds
+ `${_kTypeInfo.arrayLength}u`,
+ // Far out of bounds
+ '1000000u',
+ `${kMaxU32}u`,
+ `${kMaxI32}u`,
+ ];
+
+ const indexTypeLiteral = indexSigned ? '0' : '0u';
+ const indexTypeCast = indexSigned ? 'i32' : 'u32';
+ for (const exprIndexAddon of [
+ '', // No addon
+ ` + ${indexTypeLiteral}`, // Add a literal 0
+ ` + ${indexTypeCast}(constants.zero)`, // Add a uniform 0
+ ]) {
+ // Produce the accesses to the variable.
+ for (const indexToTest of indicesToTest) {
+ testFunctionSource += `
+ {
+ let index = (${indexToTest})${exprIndexAddon};`;
+ const exprZeroElement = `${_kTypeInfo.elementBaseType}()`;
+ const exprElement = `s.data[index]`;
+
+ switch (access) {
+ case 'read':
+ {
+ let exprLoadElement = isAtomic ? `atomicLoad(&${exprElement})` : exprElement;
+ if (storageClass === 'uniform' && containerType === 'array') {
+ // Scalar types will be wrapped in a vec4 to satisfy array element size
+ // requirements for the uniform address space, so we need an additional index
+ // accessor expression.
+ exprLoadElement += '[0]';
+ }
+ let condition = `${exprLoadElement} != ${exprZeroElement}`;
+ if (containerType === 'matrix') condition = `any(${condition})`;
+ testFunctionSource += `
+ if (${condition}) { return ${nextErrorReturnValue()}; }`;
+ }
+ break;
+
+ case 'write':
+ if (isAtomic) {
+ testFunctionSource += `
+ atomicStore(&s.data[index], ${exprZeroElement});`;
+ } else {
+ testFunctionSource += `
+ s.data[index] = ${exprZeroElement};`;
+ }
+ break;
+ }
+ testFunctionSource += `
+ }`;
+ }
+ }
+ }
+
+ // Check that the canaries haven't been modified
+ if (usesCanary) {
+ testFunctionSource += `
+ for (var i = 0u; i < 10u; i = i + 1u) {
+ if (s.startCanary[i] != 0xFFFFFFFFu) {
+ return ${nextErrorReturnValue()};
+ }
+ if (s.endCanary[i] != 0xFFFFFFFFu) {
+ return ${nextErrorReturnValue()};
+ }
+ }`;
+ }
+
+ // Shadowing case declarations
+ let moduleScopeShadowDecls = '';
+ let functionScopeShadowDecls = '';
+
+ switch (shadowingMode) {
+ case 'module-scope':
+ // Shadow the builtins likely used by robustness as module-scope variables
+ moduleScopeShadowDecls = `
+var<private> min = 0;
+var<private> max = 0;
+var<private> arrayLength = 0;
+`;
+ // Make sure that these are referenced by the function.
+ // This ensures that compilers don't strip away unused variables.
+ functionScopeShadowDecls = `
+ _ = min;
+ _ = max;
+ _ = arrayLength;
+`;
+ break;
+ case 'function-scope':
+ // Shadow the builtins likely used by robustness as function-scope variables
+ functionScopeShadowDecls = `
+ let min = 0;
+ let max = 0;
+ let arrayLength = 0;
+`;
+ break;
+ }
+
+ // Run the test
+
+ // First aggregate the test source
+ const testSource = `
+${globalSource}
+${moduleScopeShadowDecls}
+
+fn runTest() -> u32 {
+ ${functionScopeShadowDecls}
+ ${testFunctionSource}
+ return 0u;
+}`;
+
+ const layout = t.device.createPipelineLayout({
+ bindGroupLayouts: [
+ t.device.createBindGroupLayout({
+ entries: testGroupBGLEntires,
+ }),
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'uniform',
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ },
+ },
+ ],
+ }),
+ ],
+ });
+
+ // Run it.
+ if (bufferBindingSize !== undefined && baseType !== 'bool') {
+ const expectedData = new ArrayBuffer(testBufferSize);
+ const bufferBindingEnd = bufferBindingOffset + bufferBindingSize;
+ testFillArrayBuffer(expectedData, baseType, {
+ zeroByteStart: bufferBindingOffset,
+ zeroByteCount: bufferBindingSize,
+ });
+
+ // Create a buffer that contains zeroes in the allowed access area, and 42s everywhere else.
+ const testBuffer = t.makeBufferWithContents(
+ new Uint8Array(expectedData),
+ GPUBufferUsage.COPY_SRC |
+ GPUBufferUsage.UNIFORM |
+ GPUBufferUsage.STORAGE |
+ GPUBufferUsage.COPY_DST
+ );
+
+ // Run the shader, accessing the buffer.
+ runShaderTest(
+ t,
+ GPUShaderStage.COMPUTE,
+ testSource,
+ layout,
+ [
+ {
+ binding: 0,
+ resource: {
+ buffer: testBuffer,
+ offset: dynamicOffset ? 0 : bufferBindingOffset,
+ size: bufferBindingSize,
+ },
+ },
+ ],
+ dynamicOffset ? [bufferBindingOffset] : undefined
+ );
+
+ // Check that content of the buffer outside of the allowed area didn't change.
+ const expectedBytes = new Uint8Array(expectedData);
+ t.expectGPUBufferValuesEqual(testBuffer, expectedBytes.subarray(0, bufferBindingOffset), 0);
+ t.expectGPUBufferValuesEqual(
+ testBuffer,
+ expectedBytes.subarray(bufferBindingEnd, testBufferSize),
+ bufferBindingEnd
+ );
+ } else {
+ runShaderTest(t, GPUShaderStage.COMPUTE, testSource, layout, []);
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts
new file mode 100644
index 0000000000..45f7f5167f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/robust_access_vertex.spec.ts
@@ -0,0 +1,610 @@
+export const description = `
+Test vertex attributes behave correctly (no crash / data leak) when accessed out of bounds
+
+Test coverage:
+
+The following is parameterized (all combinations tested):
+
+1) Draw call type? (drawIndexed, drawIndirect, drawIndexedIndirect)
+ - Run the draw call using an index buffer and/or an indirect buffer.
+ - Doesn't test direct draw, as vertex buffer OOB are CPU validated and treated as validation errors.
+ - Also the instance step mode vertex buffer OOB are CPU validated for drawIndexed, so we only test
+ robustness access for vertex step mode vertex buffers.
+
+2) Draw call parameter (vertexCount, firstVertex, indexCount, firstIndex, baseVertex, instanceCount,
+ vertexCountInIndexBuffer)
+ - The parameter which goes out of bounds. Filtered depending on the draw call type.
+ - vertexCount, firstVertex: used for drawIndirect only, test for vertex step mode buffer OOB
+ - instanceCount: used for both drawIndirect and drawIndexedIndirect, test for instance step mode buffer OOB
+ - baseVertex, vertexCountInIndexBuffer: used for both drawIndexed and drawIndexedIndirect, test
+ for vertex step mode buffer OOB. vertexCountInIndexBuffer indicates how many vertices are used
+ within the index buffer, i.e. [0, 1, ..., vertexCountInIndexBuffer-1].
+ - indexCount, firstIndex: used for drawIndexedIndirect only, validate the vertex buffer access
+ when the vertex itself is OOB in index buffer. This never happens in drawIndexed as we have index
+ buffer OOB CPU validation for it.
+
+3) Attribute type (float32, float32x2, float32x3, float32x4)
+ - The input attribute type in the vertex shader
+
+4) Error scale (0, 1, 4, 10^2, 10^4, 10^6)
+ - Offset to add to the correct draw call parameter
+ - 0 For control case
+
+5) Additional vertex buffers (0, +4)
+ - Tests that no OOB occurs if more vertex buffers are used
+
+6) Partial last number and offset vertex buffer (false, true)
+ - Tricky cases that make vertex buffer OOB.
+ - With partial last number enabled, vertex buffer size will be 1 byte less than enough, making the
+ last vertex OOB with 1 byte.
+ - Offset vertex buffer will bind the vertex buffer to render pass with 4 bytes offset, causing OOB
+ - For drawIndexed, these two flags are suppressed for instance step mode vertex buffer to make sure
+ it pass the CPU validation.
+
+The tests have one instance step mode vertex buffer bound for instanced attributes, to make sure
+instanceCount / firstInstance are tested.
+
+The tests include multiple attributes per vertex buffer.
+
+The vertex buffers are filled by repeating a few values randomly chosen for each test until the
+end of the buffer.
+
+The tests run a render pipeline which verifies the following:
+1) All vertex attribute values occur in the buffer or are 0 (for control case it can't be 0)
+2) All gl_VertexIndex values are within the index buffer or 0
+
+TODO:
+Currently firstInstance is not tested, as for drawIndexed it is CPU validated, and for drawIndirect
+and drawIndexedIndirect it should always be 0. Once there is an extension to allow making them non-zero,
+it should be added into drawCallTestParameter list.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+
+// Encapsulates a draw call (either indexed or non-indexed)
+class DrawCall {
+ private test: GPUTest;
+ private vertexBuffers: GPUBuffer[];
+
+ // Add a float offset when binding vertex buffer
+ private offsetVertexBuffer: boolean;
+
+ // Keep instance step mode vertex buffer in range, in order to test vertex step
+ // mode buffer OOB in drawIndexed. Setting true will suppress partialLastNumber
+ // and offsetVertexBuffer for instance step mode vertex buffer.
+ private keepInstanceStepModeBufferInRange: boolean;
+
+ // Draw
+ public vertexCount: number;
+ public firstVertex: number;
+
+ // DrawIndexed
+ public vertexCountInIndexBuffer: number; // For generating index buffer in drawIndexed and drawIndexedIndirect
+ public indexCount: number; // For accessing index buffer in drawIndexed and drawIndexedIndirect
+ public firstIndex: number;
+ public baseVertex: number;
+
+ // Both Draw and DrawIndexed
+ public instanceCount: number;
+ public firstInstance: number;
+
+ constructor({
+ test,
+ vertexArrays,
+ vertexCount,
+ partialLastNumber,
+ offsetVertexBuffer,
+ keepInstanceStepModeBufferInRange,
+ }: {
+ test: GPUTest;
+ vertexArrays: Float32Array[];
+ vertexCount: number;
+ partialLastNumber: boolean;
+ offsetVertexBuffer: boolean;
+ keepInstanceStepModeBufferInRange: boolean;
+ }) {
+ this.test = test;
+
+ // Default arguments (valid call)
+ this.vertexCount = vertexCount;
+ this.firstVertex = 0;
+ this.vertexCountInIndexBuffer = vertexCount;
+ this.indexCount = vertexCount;
+ this.firstIndex = 0;
+ this.baseVertex = 0;
+ this.instanceCount = vertexCount;
+ this.firstInstance = 0;
+
+ this.offsetVertexBuffer = offsetVertexBuffer;
+ this.keepInstanceStepModeBufferInRange = keepInstanceStepModeBufferInRange;
+
+ // Since vertexInIndexBuffer is mutable, generation of the index buffer should be deferred to right before calling draw
+
+ // Generate vertex buffer
+ this.vertexBuffers = vertexArrays.map((v, i) => {
+ if (i === 0 && keepInstanceStepModeBufferInRange) {
+ // Suppress partialLastNumber for the first vertex buffer, aka the instance step mode buffer
+ return this.generateVertexBuffer(v, false);
+ } else {
+ return this.generateVertexBuffer(v, partialLastNumber);
+ }
+ });
+ }
+
+ // Insert a draw call into |pass| with specified type
+ public insertInto(pass: GPURenderPassEncoder, indexed: boolean, indirect: boolean) {
+ if (indexed) {
+ if (indirect) {
+ this.drawIndexedIndirect(pass);
+ } else {
+ this.drawIndexed(pass);
+ }
+ } else {
+ if (indirect) {
+ this.drawIndirect(pass);
+ } else {
+ this.draw(pass);
+ }
+ }
+ }
+
+ // Insert a draw call into |pass|
+ public draw(pass: GPURenderPassEncoder) {
+ this.bindVertexBuffers(pass);
+ pass.draw(this.vertexCount, this.instanceCount, this.firstVertex, this.firstInstance);
+ }
+
+ // Insert an indexed draw call into |pass|
+ public drawIndexed(pass: GPURenderPassEncoder) {
+ // Generate index buffer
+ const indexArray = new Uint32Array(this.vertexCountInIndexBuffer).map((_, i) => i);
+ const indexBuffer = this.test.makeBufferWithContents(indexArray, GPUBufferUsage.INDEX);
+ this.bindVertexBuffers(pass);
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ pass.drawIndexed(
+ this.indexCount,
+ this.instanceCount,
+ this.firstIndex,
+ this.baseVertex,
+ this.firstInstance
+ );
+ }
+
+ // Insert an indirect draw call into |pass|
+ public drawIndirect(pass: GPURenderPassEncoder) {
+ this.bindVertexBuffers(pass);
+ pass.drawIndirect(this.generateIndirectBuffer(), 0);
+ }
+
+ // Insert an indexed indirect draw call into |pass|
+ public drawIndexedIndirect(pass: GPURenderPassEncoder) {
+ // Generate index buffer
+ const indexArray = new Uint32Array(this.vertexCountInIndexBuffer).map((_, i) => i);
+ const indexBuffer = this.test.makeBufferWithContents(indexArray, GPUBufferUsage.INDEX);
+ this.bindVertexBuffers(pass);
+ pass.setIndexBuffer(indexBuffer, 'uint32');
+ pass.drawIndexedIndirect(this.generateIndexedIndirectBuffer(), 0);
+ }
+
+ // Bind all vertex buffers generated
+ private bindVertexBuffers(pass: GPURenderPassEncoder) {
+ let currSlot = 0;
+ for (let i = 0; i < this.vertexBuffers.length; i++) {
+ if (i === 0 && this.keepInstanceStepModeBufferInRange) {
+ // Keep the instance step mode buffer in range
+ pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], 0);
+ } else {
+ pass.setVertexBuffer(currSlot++, this.vertexBuffers[i], this.offsetVertexBuffer ? 4 : 0);
+ }
+ }
+ }
+
+ // Create a vertex buffer from |vertexArray|
+ // If |partialLastNumber| is true, delete one byte off the end
+ private generateVertexBuffer(vertexArray: Float32Array, partialLastNumber: boolean): GPUBuffer {
+ let size = vertexArray.byteLength;
+ let length = vertexArray.length;
+ if (partialLastNumber) {
+ size -= 1; // Shave off one byte from the buffer size.
+ length -= 1; // And one whole element from the writeBuffer.
+ }
+ const buffer = this.test.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, // Ensure that buffer can be used by writeBuffer
+ });
+ this.test.device.queue.writeBuffer(buffer, 0, vertexArray.slice(0, length));
+ return buffer;
+ }
+
+ // Create an indirect buffer containing draw call values
+ private generateIndirectBuffer(): GPUBuffer {
+ const indirectArray = new Int32Array([
+ this.vertexCount,
+ this.instanceCount,
+ this.firstVertex,
+ this.firstInstance,
+ ]);
+ return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ }
+
+ // Create an indirect buffer containing indexed draw call values
+ private generateIndexedIndirectBuffer(): GPUBuffer {
+ const indirectArray = new Int32Array([
+ this.indexCount,
+ this.instanceCount,
+ this.firstIndex,
+ this.baseVertex,
+ this.firstInstance,
+ ]);
+ return this.test.makeBufferWithContents(indirectArray, GPUBufferUsage.INDIRECT);
+ }
+}
+
+// Parameterize different sized types
+interface VertexInfo {
+ wgslType: string;
+ sizeInBytes: number;
+ validationFunc: string;
+}
+
+const typeInfoMap: { [k: string]: VertexInfo } = {
+ float32: {
+ wgslType: 'f32',
+ sizeInBytes: 4,
+ validationFunc: 'return valid(v);',
+ },
+ float32x2: {
+ wgslType: 'vec2<f32>',
+ sizeInBytes: 8,
+ validationFunc: 'return valid(v.x) && valid(v.y);',
+ },
+ float32x3: {
+ wgslType: 'vec3<f32>',
+ sizeInBytes: 12,
+ validationFunc: 'return valid(v.x) && valid(v.y) && valid(v.z);',
+ },
+ float32x4: {
+ wgslType: 'vec4<f32>',
+ sizeInBytes: 16,
+ validationFunc: `return (valid(v.x) && valid(v.y) && valid(v.z) && valid(v.w)) ||
+ (v.x == 0.0 && v.y == 0.0 && v.z == 0.0 && (v.w == 0.0 || v.w == 1.0));`,
+ },
+};
+
+class F extends GPUTest {
+ generateBufferContents(
+ numVertices: number,
+ attributesPerBuffer: number,
+ typeInfo: VertexInfo,
+ arbitraryValues: number[],
+ bufferCount: number
+ ): Float32Array[] {
+ // Make an array big enough for the vertices, attributes, and size of each element
+ const vertexArray = new Float32Array(
+ numVertices * attributesPerBuffer * (typeInfo.sizeInBytes / 4)
+ );
+
+ for (let i = 0; i < vertexArray.length; ++i) {
+ vertexArray[i] = arbitraryValues[i % arbitraryValues.length];
+ }
+
+ // Only the first buffer is instance step mode, all others are vertex step mode buffer
+ assert(bufferCount >= 2);
+ const bufferContents: Float32Array[] = [];
+ for (let i = 0; i < bufferCount; i++) {
+ bufferContents.push(vertexArray);
+ }
+
+ return bufferContents;
+ }
+
+ generateVertexBufferDescriptors(
+ bufferCount: number,
+ attributesPerBuffer: number,
+ format: GPUVertexFormat
+ ) {
+ const typeInfo = typeInfoMap[format];
+ // Vertex buffer descriptors
+ const buffers: GPUVertexBufferLayout[] = [];
+ {
+ let currAttribute = 0;
+ for (let i = 0; i < bufferCount; i++) {
+ buffers.push({
+ arrayStride: attributesPerBuffer * typeInfo.sizeInBytes,
+ stepMode: i === 0 ? 'instance' : 'vertex',
+ attributes: Array(attributesPerBuffer)
+ .fill(0)
+ .map((_, i) => ({
+ shaderLocation: currAttribute++,
+ offset: i * typeInfo.sizeInBytes,
+ format,
+ })),
+ });
+ }
+ }
+ return buffers;
+ }
+
+ generateVertexShaderCode({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ validValues: number[];
+ typeInfo: VertexInfo;
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ }): string {
+ // Create layout and attributes listing
+ let layoutStr = 'struct Attributes {';
+ const attributeNames = [];
+ {
+ let currAttribute = 0;
+ for (let i = 0; i < bufferCount; i++) {
+ for (let j = 0; j < attributesPerBuffer; j++) {
+ layoutStr += `@location(${currAttribute}) a_${currAttribute} : ${typeInfo.wgslType},\n`;
+ attributeNames.push(`a_${currAttribute}`);
+ currAttribute++;
+ }
+ }
+ }
+ layoutStr += '};';
+
+ const vertexShaderCode: string = `
+ ${layoutStr}
+
+ fn valid(f : f32) -> bool {
+ return ${validValues.map(v => `f == ${v}.0`).join(' || ')};
+ }
+
+ fn validationFunc(v : ${typeInfo.wgslType}) -> bool {
+ ${typeInfo.validationFunc}
+ }
+
+ @vertex fn main(
+ @builtin(vertex_index) VertexIndex : u32,
+ attributes : Attributes
+ ) -> @builtin(position) vec4<f32> {
+ var attributesInBounds = ${attributeNames
+ .map(a => `validationFunc(attributes.${a})`)
+ .join(' && ')};
+
+ var indexInBoundsCountFromBaseVertex =
+ (VertexIndex >= ${vertexIndexOffset}u &&
+ VertexIndex < ${vertexIndexOffset + numVertices}u);
+ var indexInBounds = VertexIndex == 0u || indexInBoundsCountFromBaseVertex;
+
+ var Position : vec4<f32>;
+ if (attributesInBounds && (${!isIndexed} || indexInBounds)) {
+ // Success case, move the vertex to the right of the viewport to show that at least one case succeed
+ Position = vec4<f32>(0.5, 0.0, 0.0, 1.0);
+ } else {
+ // Failure case, move the vertex to the left of the viewport
+ Position = vec4<f32>(-0.5, 0.0, 0.0, 1.0);
+ }
+ return Position;
+ }`;
+ return vertexShaderCode;
+ }
+
+ createRenderPipeline({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ buffers,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ validValues: number[];
+ typeInfo: VertexInfo;
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ buffers: GPUVertexBufferLayout[];
+ }): GPURenderPipeline {
+ const pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: this.device.createShaderModule({
+ code: this.generateVertexShaderCode({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ }),
+ }),
+ entryPoint: 'main',
+ buffers,
+ },
+ fragment: {
+ module: this.device.createShaderModule({
+ code: `
+ @fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+ }`,
+ }),
+ entryPoint: 'main',
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: { topology: 'point-list' },
+ });
+ return pipeline;
+ }
+
+ doTest({
+ bufferCount,
+ attributesPerBuffer,
+ dataType,
+ validValues,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ isIndirect,
+ drawCall,
+ }: {
+ bufferCount: number;
+ attributesPerBuffer: number;
+ dataType: GPUVertexFormat;
+ validValues: number[];
+ vertexIndexOffset: number;
+ numVertices: number;
+ isIndexed: boolean;
+ isIndirect: boolean;
+ drawCall: DrawCall;
+ }): void {
+ // Vertex buffer descriptors
+ const buffers: GPUVertexBufferLayout[] = this.generateVertexBufferDescriptors(
+ bufferCount,
+ attributesPerBuffer,
+ dataType
+ );
+
+ // Pipeline setup, texture setup
+ const pipeline = this.createRenderPipeline({
+ bufferCount,
+ attributesPerBuffer,
+ validValues,
+ typeInfo: typeInfoMap[dataType],
+ vertexIndexOffset,
+ numVertices,
+ isIndexed,
+ buffers,
+ });
+
+ const colorAttachment = this.device.createTexture({
+ format: 'rgba8unorm',
+ size: { width: 2, height: 1, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const colorAttachmentView = colorAttachment.createView();
+
+ const encoder = this.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ storeOp: 'store',
+ clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+
+ // Run the draw variant
+ drawCall.insertInto(pass, isIndexed, isIndirect);
+
+ pass.end();
+ this.device.queue.submit([encoder.finish()]);
+
+ // Validate we see green on the left pixel, showing that no failure case is detected
+ this.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ 'rgba8unorm',
+ { x: 0, y: 0 },
+ { exp: new Uint8Array([0x00, 0xff, 0x00, 0xff]), layout: { mipLevel: 0 } }
+ );
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('vertex_buffer_access')
+ .params(
+ u =>
+ u
+ .combineWithParams([
+ { indexed: false, indirect: true },
+ { indexed: true, indirect: false },
+ { indexed: true, indirect: true },
+ ])
+ .expand('drawCallTestParameter', function* (p) {
+ if (p.indexed) {
+ yield* ['baseVertex', 'vertexCountInIndexBuffer'] as const;
+ if (p.indirect) {
+ yield* ['indexCount', 'instanceCount', 'firstIndex'] as const;
+ }
+ } else if (p.indirect) {
+ yield* ['vertexCount', 'instanceCount', 'firstVertex'] as const;
+ }
+ })
+ .combine('type', Object.keys(typeInfoMap) as GPUVertexFormat[])
+ .combine('additionalBuffers', [0, 4])
+ .combine('partialLastNumber', [false, true])
+ .combine('offsetVertexBuffer', [false, true])
+ .combine('errorScale', [0, 1, 4, 10 ** 2, 10 ** 4, 10 ** 6])
+ .unless(p => p.drawCallTestParameter === 'instanceCount' && p.errorScale > 10 ** 4) // To avoid timeout
+ )
+ .fn(async t => {
+ const p = t.params;
+ const typeInfo = typeInfoMap[p.type];
+
+ // Number of vertices to draw
+ const numVertices = 4;
+ // Each buffer is bound to this many attributes (2 would mean 2 attributes per buffer)
+ const attributesPerBuffer = 2;
+ // Some arbitrary values to fill our buffer with to avoid collisions with other tests
+ const arbitraryValues = [990, 685, 446, 175];
+
+ // A valid value is 0 or one in the buffer
+ const validValues =
+ p.errorScale === 0 && !p.offsetVertexBuffer && !p.partialLastNumber
+ ? arbitraryValues // Control case with no OOB access, must read back valid values in buffer
+ : [0, ...arbitraryValues]; // Testing case with OOB access, can be 0 for OOB data
+
+ // Generate vertex buffer contents. Only the first buffer is instance step mode, all others are vertex step mode
+ const bufferCount = p.additionalBuffers + 2; // At least one instance step mode and one vertex step mode buffer
+ const bufferContents = t.generateBufferContents(
+ numVertices,
+ attributesPerBuffer,
+ typeInfo,
+ arbitraryValues,
+ bufferCount
+ );
+
+ // Mutable draw call
+ const draw = new DrawCall({
+ test: t,
+ vertexArrays: bufferContents,
+ vertexCount: numVertices,
+ partialLastNumber: p.partialLastNumber,
+ offsetVertexBuffer: p.offsetVertexBuffer,
+ keepInstanceStepModeBufferInRange: p.indexed && !p.indirect, // keep instance step mode buffer in range for drawIndexed
+ });
+
+ // Offset the draw call parameter we are testing by |errorScale|
+ draw[p.drawCallTestParameter] += p.errorScale;
+ // Offset the range checks for gl_VertexIndex in the shader if we use BaseVertex
+ let vertexIndexOffset = 0;
+ if (p.drawCallTestParameter === 'baseVertex') {
+ vertexIndexOffset += p.errorScale;
+ }
+
+ t.doTest({
+ bufferCount,
+ attributesPerBuffer,
+ dataType: p.type,
+ validValues,
+ vertexIndexOffset,
+ numVertices,
+ isIndexed: p.indexed,
+ isIndirect: p.indirect,
+ drawCall: draw,
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts
new file mode 100644
index 0000000000..b2ce0f9c25
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts
@@ -0,0 +1,297 @@
+export const description = `Test compute shader builtin variables`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { iterRange } from '../../../../common/util/util.js';
+import { GPUTest } from '../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// Test that the values for each input builtin are correct.
+g.test('inputs')
+ .desc(`Test compute shader builtin inputs values`)
+ .params(u =>
+ u
+ .combine('method', ['param', 'struct', 'mixed'] as const)
+ .combine('dispatch', ['direct', 'indirect'] as const)
+ .combineWithParams([
+ {
+ groupSize: { x: 1, y: 1, z: 1 },
+ numGroups: { x: 1, y: 1, z: 1 },
+ },
+ {
+ groupSize: { x: 8, y: 4, z: 2 },
+ numGroups: { x: 1, y: 1, z: 1 },
+ },
+ {
+ groupSize: { x: 1, y: 1, z: 1 },
+ numGroups: { x: 8, y: 4, z: 2 },
+ },
+ {
+ groupSize: { x: 3, y: 7, z: 5 },
+ numGroups: { x: 13, y: 9, z: 11 },
+ },
+ ] as const)
+ .beginSubcases()
+ )
+ .fn(async t => {
+ const invocationsPerGroup = t.params.groupSize.x * t.params.groupSize.y * t.params.groupSize.z;
+ const totalInvocations =
+ invocationsPerGroup * t.params.numGroups.x * t.params.numGroups.y * t.params.numGroups.z;
+
+ // Generate the structures, parameters, and builtin expressions used in the shader.
+ let params = '';
+ let structures = '';
+ let local_id = '';
+ let local_index = '';
+ let global_id = '';
+ let group_id = '';
+ let num_groups = '';
+ switch (t.params.method) {
+ case 'param':
+ params = `
+ @builtin(local_invocation_id) local_id : vec3<u32>,
+ @builtin(local_invocation_index) local_index : u32,
+ @builtin(global_invocation_id) global_id : vec3<u32>,
+ @builtin(workgroup_id) group_id : vec3<u32>,
+ @builtin(num_workgroups) num_groups : vec3<u32>,
+ `;
+ local_id = 'local_id';
+ local_index = 'local_index';
+ global_id = 'global_id';
+ group_id = 'group_id';
+ num_groups = 'num_groups';
+ break;
+ case 'struct':
+ structures = `struct Inputs {
+ @builtin(local_invocation_id) local_id : vec3<u32>,
+ @builtin(local_invocation_index) local_index : u32,
+ @builtin(global_invocation_id) global_id : vec3<u32>,
+ @builtin(workgroup_id) group_id : vec3<u32>,
+ @builtin(num_workgroups) num_groups : vec3<u32>,
+ };`;
+ params = `inputs : Inputs`;
+ local_id = 'inputs.local_id';
+ local_index = 'inputs.local_index';
+ global_id = 'inputs.global_id';
+ group_id = 'inputs.group_id';
+ num_groups = 'inputs.num_groups';
+ break;
+ case 'mixed':
+ structures = `struct InputsA {
+ @builtin(local_invocation_index) local_index : u32,
+ @builtin(global_invocation_id) global_id : vec3<u32>,
+ };
+ struct InputsB {
+ @builtin(workgroup_id) group_id : vec3<u32>
+ };`;
+ params = `@builtin(local_invocation_id) local_id : vec3<u32>,
+ inputsA : InputsA,
+ inputsB : InputsB,
+ @builtin(num_workgroups) num_groups : vec3<u32>,`;
+ local_id = 'local_id';
+ local_index = 'inputsA.local_index';
+ global_id = 'inputsA.global_id';
+ group_id = 'inputsB.group_id';
+ num_groups = 'num_groups';
+ break;
+ }
+
+ // WGSL shader that stores every builtin value to a buffer, for every invocation in the grid.
+ const wgsl = `
+ struct S {
+ data : array<u32>
+ };
+ struct V {
+ data : array<vec3<u32>>
+ };
+ @group(0) @binding(0) var<storage, read_write> local_id_out : V;
+ @group(0) @binding(1) var<storage, read_write> local_index_out : S;
+ @group(0) @binding(2) var<storage, read_write> global_id_out : V;
+ @group(0) @binding(3) var<storage, read_write> group_id_out : V;
+ @group(0) @binding(4) var<storage, read_write> num_groups_out : V;
+
+ ${structures}
+
+ const group_width = ${t.params.groupSize.x}u;
+ const group_height = ${t.params.groupSize.y}u;
+ const group_depth = ${t.params.groupSize.z}u;
+
+ @compute @workgroup_size(group_width, group_height, group_depth)
+ fn main(
+ ${params}
+ ) {
+ let group_index = ((${group_id}.z * ${num_groups}.y) + ${group_id}.y) * ${num_groups}.x + ${group_id}.x;
+ let global_index = group_index * ${invocationsPerGroup}u + ${local_index};
+ local_id_out.data[global_index] = ${local_id};
+ local_index_out.data[global_index] = ${local_index};
+ global_id_out.data[global_index] = ${global_id};
+ group_id_out.data[global_index] = ${group_id};
+ num_groups_out.data[global_index] = ${num_groups};
+ }
+ `;
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: wgsl,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ // Helper to create a `size`-byte buffer with binding number `binding`.
+ function createBuffer(size: number, binding: number) {
+ const buffer = t.device.createBuffer({
+ size,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(buffer);
+
+ bindGroupEntries.push({
+ binding,
+ resource: {
+ buffer,
+ },
+ });
+
+ return buffer;
+ }
+
+ // Create the output buffers.
+ const bindGroupEntries: GPUBindGroupEntry[] = [];
+ const localIdBuffer = createBuffer(totalInvocations * 16, 0);
+ const localIndexBuffer = createBuffer(totalInvocations * 4, 1);
+ const globalIdBuffer = createBuffer(totalInvocations * 16, 2);
+ const groupIdBuffer = createBuffer(totalInvocations * 16, 3);
+ const numGroupsBuffer = createBuffer(totalInvocations * 16, 4);
+
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: bindGroupEntries,
+ });
+
+ // Run the shader.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ switch (t.params.dispatch) {
+ case 'direct':
+ pass.dispatchWorkgroups(t.params.numGroups.x, t.params.numGroups.y, t.params.numGroups.z);
+ break;
+ case 'indirect': {
+ const dispatchBuffer = t.device.createBuffer({
+ size: 3 * Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.INDIRECT,
+ mappedAtCreation: true,
+ });
+ t.trackForCleanup(dispatchBuffer);
+ const dispatchData = new Uint32Array(dispatchBuffer.getMappedRange());
+ dispatchData[0] = t.params.numGroups.x;
+ dispatchData[1] = t.params.numGroups.y;
+ dispatchData[2] = t.params.numGroups.z;
+ dispatchBuffer.unmap();
+ pass.dispatchWorkgroupsIndirect(dispatchBuffer, 0);
+ break;
+ }
+ }
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ type vec3 = { x: number; y: number; z: number };
+
+ // Helper to check that the vec3<u32> value at each index of the provided `output` buffer
+ // matches the expected value for that invocation, as generated by the `getBuiltinValue`
+ // function. The `name` parameter is the builtin name, used for error messages.
+ const checkEachIndex = (
+ output: Uint32Array,
+ name: string,
+ getBuiltinValue: (groupId: vec3, localId: vec3) => vec3
+ ) => {
+ // Loop over workgroups.
+ for (let gz = 0; gz < t.params.numGroups.z; gz++) {
+ for (let gy = 0; gy < t.params.numGroups.y; gy++) {
+ for (let gx = 0; gx < t.params.numGroups.x; gx++) {
+ // Loop over invocations within a group.
+ for (let lz = 0; lz < t.params.groupSize.z; lz++) {
+ for (let ly = 0; ly < t.params.groupSize.y; ly++) {
+ for (let lx = 0; lx < t.params.groupSize.x; lx++) {
+ const groupIndex = (gz * t.params.numGroups.y + gy) * t.params.numGroups.x + gx;
+ const localIndex = (lz * t.params.groupSize.y + ly) * t.params.groupSize.x + lx;
+ const globalIndex = groupIndex * invocationsPerGroup + localIndex;
+ const expected = getBuiltinValue(
+ { x: gx, y: gy, z: gz },
+ { x: lx, y: ly, z: lz }
+ );
+ if (output[globalIndex * 4 + 0] !== expected.x) {
+ return new Error(
+ `${name}.x failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` +
+ ` expected: ${expected.x}\n` +
+ ` got: ${output[globalIndex * 4 + 0]}`
+ );
+ }
+ if (output[globalIndex * 4 + 1] !== expected.y) {
+ return new Error(
+ `${name}.y failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` +
+ ` expected: ${expected.y}\n` +
+ ` got: ${output[globalIndex * 4 + 1]}`
+ );
+ }
+ if (output[globalIndex * 4 + 2] !== expected.z) {
+ return new Error(
+ `${name}.z failed at group(${gx},${gy},${gz}) local(${lx},${ly},${lz}))\n` +
+ ` expected: ${expected.z}\n` +
+ ` got: ${output[globalIndex * 4 + 2]}`
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return undefined;
+ };
+
+ // Check @builtin(local_invocation_index) values.
+ t.expectGPUBufferValuesEqual(
+ localIndexBuffer,
+ new Uint32Array([...iterRange(totalInvocations, x => x % invocationsPerGroup)])
+ );
+
+ // Check @builtin(local_invocation_id) values.
+ t.expectGPUBufferValuesPassCheck(
+ localIdBuffer,
+ outputData => checkEachIndex(outputData, 'local_invocation_id', (_, localId) => localId),
+ { type: Uint32Array, typedLength: totalInvocations * 4 }
+ );
+
+ // Check @builtin(global_invocation_id) values.
+ const getGlobalId = (groupId: vec3, localId: vec3) => {
+ return {
+ x: groupId.x * t.params.groupSize.x + localId.x,
+ y: groupId.y * t.params.groupSize.y + localId.y,
+ z: groupId.z * t.params.groupSize.z + localId.z,
+ };
+ };
+ t.expectGPUBufferValuesPassCheck(
+ globalIdBuffer,
+ outputData => checkEachIndex(outputData, 'global_invocation_id', getGlobalId),
+ { type: Uint32Array, typedLength: totalInvocations * 4 }
+ );
+
+ // Check @builtin(workgroup_id) values.
+ t.expectGPUBufferValuesPassCheck(
+ groupIdBuffer,
+ outputData => checkEachIndex(outputData, 'workgroup_id', (groupId, _) => groupId),
+ { type: Uint32Array, typedLength: totalInvocations * 4 }
+ );
+
+ // Check @builtin(num_workgroups) values.
+ t.expectGPUBufferValuesPassCheck(
+ numGroupsBuffer,
+ outputData => checkEachIndex(outputData, 'num_workgroups', () => t.params.numGroups),
+ { type: Uint32Array, typedLength: totalInvocations * 4 }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/shared_structs.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/shared_structs.spec.ts
new file mode 100644
index 0000000000..f2e8cf4ece
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/shader_io/shared_structs.spec.ts
@@ -0,0 +1,353 @@
+export const description = `Test the shared use of structures containing entry point IO attributes`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../gpu_test.js';
+import { checkElementsEqual } from '../../../util/check_contents.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('shared_with_buffer')
+ .desc(
+ `Test sharing an entry point IO struct with a buffer.
+
+ This test defines a structure that contains both builtin attributes and layout attributes,
+ and uses that structure as both an entry point input and the store type of a storage buffer.
+ The builtin attributes should be ignored when used for the storage buffer, and the layout
+ attributes should be ignored when used as an entry point IO parameter.
+ `
+ )
+ .fn(async t => {
+ // Set the dispatch parameters such that we get some interesting (non-zero) built-in variables.
+ const wgsize = new Uint32Array([8, 4, 2]);
+ const numGroups = new Uint32Array([4, 2, 8]);
+
+ // Pick a single invocation to copy the input structure to the output buffer.
+ const targetLocalIndex = 13;
+ const targetGroup = new Uint32Array([2, 1, 5]);
+
+ // The test shader defines a structure that contains members decorated with built-in variable
+ // attributes, and also layout attributes for the storage buffer.
+ const wgsl = `
+ struct S {
+ /* byte offset: 0 */ @size(32) @builtin(workgroup_id) group_id : vec3<u32>,
+ /* byte offset: 32 */ @builtin(local_invocation_index) local_index : u32,
+ /* byte offset: 64 */ @align(64) @builtin(num_workgroups) numGroups : vec3<u32>,
+ };
+
+ @group(0) @binding(0)
+ var<storage, read_write> outputs : S;
+
+ @compute @workgroup_size(${wgsize[0]}, ${wgsize[1]}, ${wgsize[2]})
+ fn main(inputs : S) {
+ if (inputs.group_id.x == ${targetGroup[0]}u &&
+ inputs.group_id.y == ${targetGroup[1]}u &&
+ inputs.group_id.z == ${targetGroup[2]}u &&
+ inputs.local_index == ${targetLocalIndex}u) {
+ outputs = inputs;
+ }
+ }
+ `;
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ entryPoint: 'main',
+ },
+ });
+
+ // Allocate a buffer to hold the output structure.
+ const bufferNumElements = 32;
+ const outputBuffer = t.device.createBuffer({
+ size: bufferNumElements * Uint32Array.BYTES_PER_ELEMENT,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: outputBuffer } }],
+ });
+
+ // Run the shader.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(numGroups[0], numGroups[1], numGroups[2]);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // Check the output values.
+ const checkOutput = (outputs: Uint32Array) => {
+ if (checkElementsEqual(outputs.slice(0, 3), targetGroup)) {
+ return new Error(
+ `group_id comparison failed\n` +
+ ` expected: ${targetGroup}\n` +
+ ` got: ${outputs.slice(0, 3)}`
+ );
+ }
+ if (outputs[8] !== targetLocalIndex) {
+ return new Error(
+ `local_index comparison failed\n` +
+ ` expected: ${targetLocalIndex}\n` +
+ ` got: ${outputs[8]}`
+ );
+ }
+ if (checkElementsEqual(outputs.slice(16, 19), numGroups)) {
+ return new Error(
+ `numGroups comparison failed\n` +
+ ` expected: ${numGroups}\n` +
+ ` got: ${outputs.slice(16, 19)}`
+ );
+ }
+ return undefined;
+ };
+ t.expectGPUBufferValuesPassCheck(outputBuffer, outputData => checkOutput(outputData), {
+ type: Uint32Array,
+ typedLength: bufferNumElements,
+ });
+ });
+
+g.test('shared_between_stages')
+ .desc(
+ `Test sharing an entry point IO struct between different pipeline stages.
+
+ This test defines an entry point IO structure, and uses it as both the output of a vertex
+ shader and the input to a fragment shader.
+ `
+ )
+ .fn(async t => {
+ const size = [31, 31];
+ const wgsl = `
+ struct Interface {
+ @builtin(position) position : vec4<f32>,
+ @location(0) color : f32,
+ };
+
+ var<private> vertices : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-0.7, -0.7),
+ vec2<f32>( 0.0, 0.7),
+ vec2<f32>( 0.7, -0.7),
+ );
+
+ @vertex
+ fn vert_main(@builtin(vertex_index) index : u32) -> Interface {
+ return Interface(vec4<f32>(vertices[index], 0.0, 1.0), 1.0);
+ }
+
+ @fragment
+ fn frag_main(inputs : Interface) -> @location(0) vec4<f32> {
+ // Toggle red vs green based on the x position.
+ var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ if (inputs.position.x > f32(${size[0] / 2})) {
+ color.r = inputs.color;
+ } else {
+ color.g = inputs.color;
+ }
+ return color;
+ }
+ `;
+
+ // Set up the render pipeline.
+ const module = t.device.createShaderModule({ code: wgsl });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+ targets: [
+ {
+ format: 'rgba8unorm',
+ },
+ ],
+ },
+ });
+
+ // Draw a red triangle.
+ const renderTarget = t.device.createTexture({
+ size,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // Test a few points to make sure we rendered a half-red/half-green triangle.
+ const redPixel = new Uint8Array([255, 0, 0, 255]);
+ const greenPixel = new Uint8Array([0, 255, 0, 255]);
+ for (const p of [
+ { x: 16, y: 15 },
+ { x: 16, y: 15 },
+ { x: 22, y: 20 },
+ ]) {
+ t.expectSinglePixelIn2DTexture(renderTarget, 'rgba8unorm', p, {
+ exp: redPixel,
+ });
+ }
+ for (const p of [
+ { x: 14, y: 15 },
+ { x: 14, y: 8 },
+ { x: 8, y: 20 },
+ ]) {
+ t.expectSinglePixelIn2DTexture(renderTarget, 'rgba8unorm', p, {
+ exp: greenPixel,
+ });
+ }
+ const blackPixel = new Uint8Array([0, 0, 0, 0]);
+ for (const p of [
+ { x: 2, y: 2 },
+ { x: 2, y: 28 },
+ { x: 28, y: 2 },
+ { x: 28, y: 28 },
+ ]) {
+ t.expectSinglePixelIn2DTexture(renderTarget, 'rgba8unorm', p, {
+ exp: blackPixel,
+ });
+ }
+ });
+
+g.test('shared_with_non_entry_point_function')
+ .desc(
+ `Test sharing an entry point IO struct with a non entry point function.
+
+ This test defines structures that contain builtin and location attributes, and uses those
+ structures as parameter and return types for entry point functions and regular functions.
+ `
+ )
+ .fn(async t => {
+ // The test shader defines structures that contain members decorated with built-in variable
+ // attributes and user-defined IO. These structures are passed to and returned from regular
+ // functions.
+ const wgsl = `
+ struct Inputs {
+ @builtin(vertex_index) index : u32,
+ @location(0) color : vec4<f32>,
+ };
+ struct Outputs {
+ @builtin(position) position : vec4<f32>,
+ @location(0) color : vec4<f32>,
+ };
+
+ var<private> vertices : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+ vec2<f32>(-0.7, -0.7),
+ vec2<f32>( 0.0, 0.7),
+ vec2<f32>( 0.7, -0.7),
+ );
+
+ fn process(in : Inputs) -> Outputs {
+ var out : Outputs;
+ out.position = vec4<f32>(vertices[in.index], 0.0, 1.0);
+ out.color = in.color;
+ return out;
+ }
+
+ @vertex
+ fn vert_main(inputs : Inputs) -> Outputs {
+ return process(inputs);
+ }
+
+ @fragment
+ fn frag_main(@location(0) color : vec4<f32>) -> @location(0) vec4<f32> {
+ return color;
+ }
+ `;
+
+ // Set up the render pipeline.
+ const module = t.device.createShaderModule({ code: wgsl });
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vert_main',
+ buffers: [
+ {
+ attributes: [
+ {
+ shaderLocation: 0,
+ format: 'float32x4',
+ offset: 0,
+ },
+ ],
+ arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT,
+ },
+ ],
+ },
+ fragment: {
+ module,
+ entryPoint: 'frag_main',
+ targets: [
+ {
+ format: 'rgba8unorm',
+ },
+ ],
+ },
+ });
+
+ // Draw a triangle.
+ // The vertex buffer contains the vertex colors (all red).
+ const vertexBuffer = t.makeBufferWithContents(
+ new Float32Array([1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0]),
+ GPUBufferUsage.VERTEX
+ );
+ const renderTarget = t.device.createTexture({
+ size: [31, 31],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ format: 'rgba8unorm',
+ });
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setVertexBuffer(0, vertexBuffer);
+ pass.draw(3);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // Test a few points to make sure we rendered a red triangle.
+ const redPixel = new Uint8Array([255, 0, 0, 255]);
+ for (const p of [
+ { x: 15, y: 15 },
+ { x: 15, y: 8 },
+ { x: 8, y: 20 },
+ { x: 22, y: 20 },
+ ]) {
+ t.expectSinglePixelIn2DTexture(renderTarget, 'rgba8unorm', p, {
+ exp: redPixel,
+ });
+ }
+ const blackPixel = new Uint8Array([0, 0, 0, 0]);
+ for (const p of [
+ { x: 2, y: 2 },
+ { x: 2, y: 28 },
+ { x: 28, y: 2 },
+ { x: 28, y: 28 },
+ ]) {
+ t.expectSinglePixelIn2DTexture(renderTarget, 'rgba8unorm', p, {
+ exp: blackPixel,
+ });
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts
new file mode 100644
index 0000000000..c510217ab1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/execution/zero_init.spec.ts
@@ -0,0 +1,448 @@
+export const description = `Test that variables in the shader are zero initialized`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { unreachable } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+import {
+ ScalarType,
+ kVectorContainerTypes,
+ kVectorContainerTypeInfo,
+ kMatrixContainerTypes,
+ kMatrixContainerTypeInfo,
+ supportedScalarTypes,
+ supportsAtomics,
+} from '../types.js';
+
+type ShaderTypeInfo =
+ | { type: 'container'; containerType: 'array'; elementType: ShaderTypeInfo; length: number }
+ | { type: 'container'; containerType: 'struct'; members: ShaderTypeInfo[] }
+ | {
+ type: 'container';
+ containerType: keyof typeof kVectorContainerTypeInfo | keyof typeof kMatrixContainerTypeInfo;
+ scalarType: ScalarType;
+ }
+ | { type: 'scalar'; scalarType: ScalarType; isAtomic: boolean };
+
+function prettyPrint(t: ShaderTypeInfo): string {
+ switch (t.type) {
+ case 'container':
+ switch (t.containerType) {
+ case 'array':
+ return `array<${prettyPrint(t.elementType)}, ${t.length}>`;
+ case 'struct':
+ return `struct { ${t.members.map(m => prettyPrint(m)).join(', ')} }`;
+ default:
+ return `${t.containerType}<${prettyPrint({
+ type: 'scalar',
+ scalarType: t.scalarType,
+ isAtomic: false,
+ })}>`;
+ }
+ break;
+ case 'scalar':
+ if (t.isAtomic) {
+ return `atomic<${t.scalarType}>`;
+ }
+ return t.scalarType;
+ }
+}
+
+export const g = makeTestGroup(GPUTest);
+g.test('compute,zero_init')
+ .desc(
+ `Test that uninitialized variables in workgroup, private, and function storage classes are initialized to zero.
+
+ TODO: Run a shader before the test to attempt to fill memory with garbage`
+ )
+ .params(u =>
+ u
+ // Only workgroup, function, and private variables can be declared without data bound to them.
+ // The implementation's shader translator should ensure these values are initialized.
+ .combine('storageClass', ['workgroup', 'private', 'function'] as const)
+ .expand('workgroupSize', ({ storageClass }) => {
+ switch (storageClass) {
+ case 'workgroup':
+ return [
+ [1, 1, 1],
+ [1, 32, 1],
+ [64, 1, 1],
+ [1, 1, 48],
+ [1, 47, 1],
+ [33, 1, 1],
+ [1, 1, 63],
+ [8, 8, 2],
+ [7, 7, 3],
+ ];
+ case 'function':
+ case 'private':
+ return [[1, 1, 1]];
+ }
+ })
+ .beginSubcases()
+ // Fewer subcases: Only 0 and 2. If double-nested containers work, single-nested should too.
+ .combine('_containerDepth', [0, 2])
+ .expandWithParams(function* (p) {
+ const kElementCounts = [
+ [], // Not used. Depth 0 is always scalars.
+ [1, 3, 67], // Test something above the workgroup size.
+ [1, 3],
+ ] as const;
+ const kMemberCounts = [1, 3] as const;
+
+ const memoizedTypes: ShaderTypeInfo[][] = [];
+
+ function generateTypesMemo(depth: number): ShaderTypeInfo[] {
+ if (memoizedTypes[depth] === undefined) {
+ memoizedTypes[depth] = Array.from(generateTypes(depth));
+ }
+ return memoizedTypes[depth];
+ }
+
+ function* generateTypes(depth: number): Generator<ShaderTypeInfo> {
+ if (depth === 0) {
+ for (const isAtomic of supportsAtomics({
+ ...p,
+ access: 'read_write',
+ storageMode: undefined,
+ containerType: 'scalar',
+ })
+ ? [true, false]
+ : [false]) {
+ for (const scalarType of supportedScalarTypes({ isAtomic, ...p })) {
+ // Fewer subcases: For nested types, skip atomic u32 and non-atomic i32.
+ if (p._containerDepth > 0) {
+ if (scalarType === 'u32' && isAtomic) continue;
+ if (scalarType === 'i32' && !isAtomic) continue;
+ }
+
+ yield {
+ type: 'scalar',
+ scalarType,
+ isAtomic,
+ };
+ if (!isAtomic) {
+ // Vector types
+ for (const vectorType of kVectorContainerTypes) {
+ // Fewer subcases: For nested types, only include
+ // vec2<u32>, vec3<i32>, and vec4<f32>
+ if (p._containerDepth > 0) {
+ if (
+ !(
+ (vectorType === 'vec2' && scalarType === 'u32') ||
+ (vectorType === 'vec3' && scalarType === 'i32') ||
+ (vectorType === 'vec4' && scalarType === 'f32')
+ )
+ ) {
+ continue;
+ }
+ }
+ yield {
+ type: 'container',
+ containerType: vectorType,
+ scalarType,
+ };
+ }
+ // Matrices can only be f32.
+ if (scalarType === 'f32') {
+ for (const matrixType of kMatrixContainerTypes) {
+ yield {
+ type: 'container',
+ containerType: matrixType,
+ scalarType,
+ };
+ }
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ for (const containerType of ['array', 'struct']) {
+ const innerTypes = generateTypesMemo(depth - 1);
+ switch (containerType) {
+ case 'array':
+ for (const elementCount of kElementCounts[depth]) {
+ for (const innerType of innerTypes) {
+ yield {
+ type: 'container',
+ containerType,
+ elementType: innerType,
+ length: elementCount,
+ };
+ }
+ }
+ break;
+ case 'struct':
+ for (const memberCount of kMemberCounts) {
+ const memberIndices = new Array(memberCount);
+ for (let m = 0; m < memberCount; ++m) {
+ memberIndices[m] = m;
+ }
+
+ // Don't generate all possible combinations of inner struct members,
+ // because that's in the millions. Instead, just round-robin through
+ // to pick member types. Loop through the types, concatenated forward
+ // and backward, three times to produce a bounded but variable set of
+ // types.
+ const memberTypes = [...innerTypes, ...[...innerTypes].reverse()];
+ const seenTypes = new Set();
+ let typeIndex = 0;
+ while (typeIndex < memberTypes.length * 3) {
+ const prevTypeIndex = typeIndex;
+ const members: ShaderTypeInfo[] = [];
+ for (const m of memberIndices) {
+ members[m] = memberTypes[typeIndex % memberTypes.length];
+ typeIndex += 1;
+ }
+
+ const t: ShaderTypeInfo = {
+ type: 'container',
+ containerType,
+ members,
+ };
+ const serializedT = prettyPrint(t);
+ if (seenTypes.has(serializedT)) {
+ // We produced an identical type. shuffle the member indices,
+ // "revert" typeIndex back to where it was before this loop, and
+ // shift it by one. This helps ensure we don't loop forever, and
+ // that we produce a different type on the next iteration.
+ memberIndices.push(memberIndices.shift());
+ typeIndex = prevTypeIndex + 1;
+ continue;
+ }
+ seenTypes.add(serializedT);
+ yield t;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ for (const t of generateTypesMemo(p._containerDepth)) {
+ yield {
+ shaderTypeParam: prettyPrint(t),
+ _type: t,
+ };
+ }
+ })
+ )
+ .batch(15)
+ .fn(async t => {
+ let moduleScope = `
+ struct Output {
+ failed : atomic<u32>
+ }
+ @group(0) @binding(0) var<storage, read_write> output : Output;
+
+ // This uniform value that's a zero is used to prevent the shader compilers from trying to
+ // unroll the massive loops generated by these tests.
+ @group(0) @binding(1) var<uniform> zero : u32;
+ `;
+ let functionScope = '';
+
+ const declaredStructTypes = new Map<ShaderTypeInfo, string>();
+ const typeDecl = (function ensureType(
+ typeName: string,
+ type: ShaderTypeInfo,
+ depth: number = 0
+ ): string {
+ switch (type.type) {
+ case 'container':
+ switch (type.containerType) {
+ case 'array':
+ return `array<${ensureType(
+ `${typeName}_ArrayElement`,
+ type.elementType,
+ depth + 1
+ )}, ${type.length}>`;
+ case 'struct': {
+ if (declaredStructTypes.has(type)) {
+ return declaredStructTypes.get(type)!;
+ }
+
+ const members = type.members
+ .map((member, i) => {
+ return `\n member${i} : ${ensureType(
+ `${typeName}_Member${i}`,
+ member,
+ depth + 1
+ )},`;
+ })
+ .join('');
+ declaredStructTypes.set(type, typeName);
+ moduleScope += `\nstruct ${typeName} {`;
+ moduleScope += members;
+ moduleScope += '\n};';
+
+ return typeName;
+ }
+ default:
+ return `${type.containerType}<${ensureType(
+ typeName,
+ {
+ type: 'scalar',
+ scalarType: type.scalarType,
+ isAtomic: false,
+ },
+ depth + 1
+ )}>`;
+ }
+ break;
+ case 'scalar':
+ return type.isAtomic ? `atomic<${type.scalarType}>` : type.scalarType;
+ }
+ })('TestType', t.params._type);
+
+ switch (t.params.storageClass) {
+ case 'workgroup':
+ case 'private':
+ moduleScope += `\nvar<${t.params.storageClass}> testVar: ${typeDecl};`;
+ break;
+ case 'function':
+ functionScope += `\nvar testVar: ${typeDecl};`;
+ break;
+ }
+
+ const checkZeroCode = (function checkZero(
+ value: string,
+ type: ShaderTypeInfo,
+ depth: number = 0
+ ): string {
+ switch (type.type) {
+ case 'container':
+ switch (type.containerType) {
+ case 'array':
+ return `\nfor (var i${depth} = 0u; i${depth} < ${
+ type.length
+ }u + zero; i${depth} = i${depth} + 1u) {
+ ${checkZero(`${value}[i${depth}]`, type.elementType, depth + 1)}
+ }`;
+ case 'struct':
+ return type.members
+ .map((member, i) => {
+ return checkZero(`${value}.member${i}`, member, depth + 1);
+ })
+ .join('\n');
+ default:
+ if (type.containerType.indexOf('vec') !== -1) {
+ const length = type.containerType[3];
+ return `\nfor (var i${depth} = 0u; i${depth} < ${length}u + zero; i${depth} = i${depth} + 1u) {
+ ${checkZero(
+ `${value}[i${depth}]`,
+ {
+ type: 'scalar',
+ scalarType: type.scalarType,
+ isAtomic: false,
+ },
+ depth + 1
+ )}
+ }`;
+ } else if (type.containerType.indexOf('mat') !== -1) {
+ const cols = type.containerType[3];
+ const rows = type.containerType[5];
+ return `\nfor (var c${depth} = 0u; c${depth} < ${cols}u + zero; c${depth} = c${depth} + 1u) {
+ for (var r${depth} = 0u; r${depth} < ${rows}u; r${depth} = r${depth} + 1u) {
+ ${checkZero(
+ `${value}[c${depth}][r${depth}]`,
+ {
+ type: 'scalar',
+ scalarType: type.scalarType,
+ isAtomic: false,
+ },
+ depth + 1
+ )}
+ }
+ }`;
+ } else {
+ unreachable();
+ }
+ }
+ break;
+ case 'scalar': {
+ let expected;
+ switch (type.scalarType) {
+ case 'bool':
+ expected = 'false';
+ break;
+ case 'f32':
+ expected = '0.0';
+ break;
+ case 'i32':
+ expected = '0';
+ break;
+ case 'u32':
+ expected = '0u';
+ break;
+ }
+ if (type.isAtomic) {
+ value = `atomicLoad(&${value})`;
+ }
+
+ // Note: this could have an early return, but we omit it because it makes
+ // the tests fail cause with DXGI_ERROR_DEVICE_HUNG on Windows.
+ return `\nif (${value} != ${expected}) { atomicStore(&output.failed, 1u); }`;
+ }
+ }
+ })('testVar', t.params._type);
+
+ const wgsl = `
+ ${moduleScope}
+ @compute @workgroup_size(${t.params.workgroupSize})
+ fn main() {
+ ${functionScope}
+ ${checkZeroCode}
+ _ = zero;
+ }
+ `;
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: wgsl,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const resultBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ t.trackForCleanup(resultBuffer);
+
+ const zeroBuffer = t.device.createBuffer({
+ size: 4,
+ usage: GPUBufferUsage.UNIFORM,
+ });
+ t.trackForCleanup(zeroBuffer);
+
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: resultBuffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: zeroBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+ t.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array([0]));
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/regression/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/regression/README.txt
new file mode 100644
index 0000000000..eff2f830eb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/regression/README.txt
@@ -0,0 +1,2 @@
+One-off tests that reproduce shader bugs found in implementations to prevent the bugs from
+appearing again.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts
new file mode 100644
index 0000000000..7cd5e43302
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/types.ts
@@ -0,0 +1,209 @@
+import { keysOf } from '../../common/util/data_tables.js';
+import { assert } from '../../common/util/util.js';
+import { align } from '../util/math.js';
+
+const kArrayLength = 3;
+
+export type ContainerType = 'scalar' | 'vector' | 'matrix' | 'atomic' | 'array';
+export type ScalarType = 'i32' | 'u32' | 'f32' | 'bool';
+
+export const HostSharableTypes = ['i32', 'u32', 'f32'] as const;
+
+/** Info for each plain scalar type. */
+export const kScalarTypeInfo = /* prettier-ignore */ {
+ 'i32': { layout: { alignment: 4, size: 4 }, supportsAtomics: true, arrayLength: 1, innerLength: 0 },
+ 'u32': { layout: { alignment: 4, size: 4 }, supportsAtomics: true, arrayLength: 1, innerLength: 0 },
+ 'f32': { layout: { alignment: 4, size: 4 }, supportsAtomics: false, arrayLength: 1, innerLength: 0 },
+ 'bool': { layout: undefined, supportsAtomics: false, arrayLength: 1, innerLength: 0 },
+} as const;
+/** List of all plain scalar types. */
+export const kScalarTypes = keysOf(kScalarTypeInfo);
+
+/** Info for each vecN<> container type. */
+export const kVectorContainerTypeInfo = /* prettier-ignore */ {
+ 'vec2': { layout: { alignment: 8, size: 8 }, arrayLength: 2 , innerLength: 0 },
+ 'vec3': { layout: { alignment: 16, size: 12 }, arrayLength: 3 , innerLength: 0 },
+ 'vec4': { layout: { alignment: 16, size: 16 }, arrayLength: 4 , innerLength: 0 },
+} as const;
+/** List of all vecN<> container types. */
+export const kVectorContainerTypes = keysOf(kVectorContainerTypeInfo);
+
+/** Info for each matNxN<> container type. */
+export const kMatrixContainerTypeInfo = /* prettier-ignore */ {
+ 'mat2x2': { layout: { alignment: 8, size: 16 }, arrayLength: 2, innerLength: 2 },
+ 'mat3x2': { layout: { alignment: 8, size: 24 }, arrayLength: 3, innerLength: 2 },
+ 'mat4x2': { layout: { alignment: 8, size: 32 }, arrayLength: 4, innerLength: 2 },
+ 'mat2x3': { layout: { alignment: 16, size: 32 }, arrayLength: 2, innerLength: 3 },
+ 'mat3x3': { layout: { alignment: 16, size: 48 }, arrayLength: 3, innerLength: 3 },
+ 'mat4x3': { layout: { alignment: 16, size: 64 }, arrayLength: 4, innerLength: 3 },
+ 'mat2x4': { layout: { alignment: 16, size: 32 }, arrayLength: 2, innerLength: 4 },
+ 'mat3x4': { layout: { alignment: 16, size: 48 }, arrayLength: 3, innerLength: 4 },
+ 'mat4x4': { layout: { alignment: 16, size: 64 }, arrayLength: 4, innerLength: 4 },
+} as const;
+/** List of all matNxN<> container types. */
+export const kMatrixContainerTypes = keysOf(kMatrixContainerTypeInfo);
+
+export type StorageClass = 'storage' | 'uniform' | 'private' | 'function' | 'workgroup';
+
+/** List of texel formats and their shader representation */
+export const TexelFormats = [
+ { format: 'rgba8unorm', _shaderType: 'f32' },
+ { format: 'rgba8snorm', _shaderType: 'f32' },
+ { format: 'rgba8uint', _shaderType: 'u32' },
+ { format: 'rgba8sint', _shaderType: 'i32' },
+ { format: 'rgba16uint', _shaderType: 'u32' },
+ { format: 'rgba16sint', _shaderType: 'i32' },
+ { format: 'rgba16float', _shaderType: 'f32' },
+ { format: 'r32uint', _shaderType: 'u32' },
+ { format: 'r32sint', _shaderType: 'i32' },
+ { format: 'r32float', _shaderType: 'f32' },
+ { format: 'rg32uint', _shaderType: 'u32' },
+ { format: 'rg32sint', _shaderType: 'i32' },
+ { format: 'rg32float', _shaderType: 'f32' },
+ { format: 'rgba32uint', _shaderType: 'i32' },
+ { format: 'rgba32sint', _shaderType: 'i32' },
+ { format: 'rgba32float', _shaderType: 'f32' },
+] as const;
+
+/**
+ * Generate a bunch types (vec, mat, sized/unsized array) for testing.
+ */
+export function* generateTypes({
+ storageClass,
+ baseType,
+ containerType,
+ isAtomic = false,
+}: {
+ storageClass: StorageClass;
+ /** Base scalar type (i32/u32/f32/bool). */
+ baseType: ScalarType;
+ /** Container type (scalar/vector/matrix/array) */
+ containerType: ContainerType;
+ /** Whether to wrap the baseType in `atomic<>`. */
+ isAtomic?: boolean;
+}) {
+ const scalarInfo = kScalarTypeInfo[baseType];
+ if (isAtomic) {
+ assert(scalarInfo.supportsAtomics, 'type does not support atomics');
+ }
+ const scalarType = isAtomic ? `atomic<${baseType}>` : baseType;
+
+ // Storage and uniform require host-sharable types.
+ if (storageClass === 'storage' || storageClass === 'uniform') {
+ assert(isHostSharable(baseType), 'type ' + baseType.toString() + ' is not host sharable');
+ }
+
+ // Scalar types
+ if (containerType === 'scalar') {
+ yield {
+ type: `${scalarType}`,
+ _kTypeInfo: {
+ elementBaseType: `${scalarType}`,
+ ...scalarInfo,
+ },
+ };
+ }
+
+ // Vector types
+ if (containerType === 'vector') {
+ for (const vectorType of kVectorContainerTypes) {
+ yield {
+ type: `${vectorType}<${scalarType}>`,
+ _kTypeInfo: { elementBaseType: baseType, ...kVectorContainerTypeInfo[vectorType] },
+ };
+ }
+ }
+
+ if (containerType === 'matrix') {
+ // Matrices can only be f32.
+ if (baseType === 'f32') {
+ for (const matrixType of kMatrixContainerTypes) {
+ const matrixInfo = kMatrixContainerTypeInfo[matrixType];
+ yield {
+ type: `${matrixType}<${scalarType}>`,
+ _kTypeInfo: {
+ elementBaseType: `vec${matrixInfo.innerLength}<${scalarType}>`,
+ ...matrixInfo,
+ },
+ };
+ }
+ }
+ }
+
+ // Array types
+ if (containerType === 'array') {
+ const arrayTypeInfo = {
+ elementBaseType: `${baseType}`,
+ arrayLength: kArrayLength,
+ layout: scalarInfo.layout
+ ? {
+ alignment: scalarInfo.layout.alignment,
+ size:
+ storageClass === 'uniform'
+ ? // Uniform storage class must have array elements aligned to 16.
+ kArrayLength *
+ arrayStride({
+ ...scalarInfo.layout,
+ alignment: 16,
+ })
+ : kArrayLength * arrayStride(scalarInfo.layout),
+ }
+ : undefined,
+ };
+
+ // Sized
+ if (storageClass === 'uniform') {
+ yield {
+ type: `array<vec4<${scalarType}>,${kArrayLength}>`,
+ _kTypeInfo: arrayTypeInfo,
+ };
+ } else {
+ yield { type: `array<${scalarType},${kArrayLength}>`, _kTypeInfo: arrayTypeInfo };
+ }
+ // Unsized
+ if (storageClass === 'storage') {
+ yield { type: `array<${scalarType}>`, _kTypeInfo: arrayTypeInfo };
+ }
+ }
+
+ function arrayStride(elementLayout: { size: number; alignment: number }) {
+ return align(elementLayout.size, elementLayout.alignment);
+ }
+
+ function isHostSharable(baseType: ScalarType) {
+ for (const sharableType of HostSharableTypes) {
+ if (sharableType === baseType) return true;
+ }
+ return false;
+ }
+}
+
+/** Atomic access requires scalar/array container type and storage/workgroup memory. */
+export function supportsAtomics(p: {
+ storageClass: string;
+ storageMode: string | undefined;
+ access: string;
+ containerType: ContainerType;
+}) {
+ return (
+ ((p.storageClass === 'storage' && p.storageMode === 'read_write') ||
+ p.storageClass === 'workgroup') &&
+ (p.containerType === 'scalar' || p.containerType === 'array')
+ );
+}
+
+/** Generates an iterator of supported base types (i32/u32/f32/bool) */
+export function* supportedScalarTypes(p: { isAtomic: boolean; storageClass: string }) {
+ for (const scalarType of kScalarTypes) {
+ const info = kScalarTypeInfo[scalarType];
+
+ // Test atomics only on supported scalar types.
+ if (p.isAtomic && !info.supportsAtomics) continue;
+
+ // Storage and uniform require host-sharable types.
+ const isHostShared = p.storageClass === 'storage' || p.storageClass === 'uniform';
+ if (isHostShared && info.layout === undefined) continue;
+
+ yield scalarType;
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/README.txt
new file mode 100644
index 0000000000..3fd3b075ae
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/README.txt
@@ -0,0 +1 @@
+Positive and negative tests for all the validation rules of the shading language.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/align.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/align.spec.ts
new file mode 100644
index 0000000000..1bb3e56f8c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/align.spec.ts
@@ -0,0 +1,180 @@
+export const description = `Validation tests for @align`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kValidAlign = new Set([
+ '',
+ '@align(1)',
+ '@align(4)',
+ '@align(4i)',
+ '@align(4u)',
+ '@align(0x4)',
+ '@align(4,)',
+ '@align(u_val)',
+ '@align(i_val)',
+ '@align(i_val + 4 - 6)',
+ '@align(1073741824)',
+ '@\talign\t(4)',
+ '@/^comment^/align/^comment^/(4)',
+]);
+const kInvalidAlign = new Set([
+ '@malign(4)',
+ '@align()',
+ '@align 4)',
+ '@align(4',
+ '@align(4, 2)',
+ '@align(4,)',
+ '@align(3)', // Not a power of 2
+ '@align(f_val)',
+ '@align(1.0)',
+ '@align(4f)',
+ '@align(4h)',
+ '@align',
+ '@align(0)',
+ '@align(-4)',
+ '@align(2147483646)', // Not a power of 2
+ '@align(2147483648)', // Larger then max i32
+]);
+
+g.test('align_parsing')
+ .desc(`Test that @align is parsed correctly.`)
+ .params(u => u.combine('align', new Set([...kValidAlign, ...kInvalidAlign])))
+ .fn(t => {
+ const v = t.params.align.replace(/\^/g, '*');
+ const code = `
+const i_val: i32 = 4;
+const u_val: u32 = 4;
+const f_val: f32 = 4.2;
+struct B {
+ ${v} a: i32,
+}
+
+@group(0) @binding(0)
+var<uniform> uniform_buffer: B;
+
+@fragment
+fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(.4, .2, .3, .1);
+}`;
+ t.expectCompileResult(kValidAlign.has(t.params.align), code);
+ });
+
+g.test('align_required_alignment')
+ .desc('Test that the align with an invalid size is an error')
+ .params(u =>
+ u
+ .combine('address_space', ['storage', 'uniform'])
+ // These test a few cases:
+ // * 1 -- Invalid, alignment smaller then all the required alignments
+ // * alignment -- Valid, the required alignment
+ // * 32 -- Valid, an alignment larger then the required alignment.
+ .combine('align', [1, 2, 'alignment', 32])
+ .combine('type', [
+ { name: 'i32', storage: 4, uniform: 4 },
+ { name: 'u32', storage: 4, uniform: 4 },
+ { name: 'f32', storage: 4, uniform: 4 },
+ { name: 'f16', storage: 2, uniform: 2 },
+ { name: 'atomic<i32>', storage: 4, uniform: 4 },
+ { name: 'vec2<i32>', storage: 8, uniform: 8 },
+ { name: 'vec2<f16>', storage: 4, uniform: 4 },
+ { name: 'vec3<u32>', storage: 16, uniform: 16 },
+ { name: 'vec3<f16>', storage: 8, uniform: 8 },
+ { name: 'vec4<f32>', storage: 16, uniform: 16 },
+ { name: 'vec4<f16>', storage: 8, uniform: 8 },
+ { name: 'mat2x2<f32>', storage: 8, uniform: 8 },
+ { name: 'mat3x2<f32>', storage: 8, uniform: 8 },
+ { name: 'mat4x2<f32>', storage: 8, uniform: 8 },
+ { name: 'mat2x2<f16>', storage: 4, uniform: 4 },
+ { name: 'mat3x2<f16>', storage: 4, uniform: 4 },
+ { name: 'mat4x2<f16>', storage: 4, uniform: 4 },
+ { name: 'mat2x3<f32>', storage: 16, uniform: 16 },
+ { name: 'mat3x3<f32>', storage: 16, uniform: 16 },
+ { name: 'mat4x3<f32>', storage: 16, uniform: 16 },
+ { name: 'mat2x3<f16>', storage: 8, uniform: 8 },
+ { name: 'mat3x3<f16>', storage: 8, uniform: 8 },
+ { name: 'mat4x3<f16>', storage: 8, uniform: 8 },
+ { name: 'mat2x4<f32>', storage: 16, uniform: 16 },
+ { name: 'mat3x4<f32>', storage: 16, uniform: 16 },
+ { name: 'mat4x4<f32>', storage: 16, uniform: 16 },
+ { name: 'mat2x4<f16>', storage: 8, uniform: 8 },
+ { name: 'mat3x4<f16>', storage: 8, uniform: 8 },
+ { name: 'mat4x4<f16>', storage: 8, uniform: 8 },
+ { name: 'array<vec2<i32>, 2>', storage: 8, uniform: 16 },
+ { name: 'array<vec4<i32>, 2>', storage: 8, uniform: 16 },
+ { name: 'S', storage: 8, uniform: 16 },
+ ])
+ .beginSubcases()
+ )
+ .beforeAllSubcases(t => {
+ if (t.params.type.name.includes('f16')) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ // While this would fail validation, it doesn't fail for any reasons related to alignment.
+ // Atomics are not allowed in uniform address space as they have to be read_write.
+ if (t.params.address_space === 'uniform' && t.params.type.name.startsWith('atomic')) {
+ t.skip('No atomics in uniform address space');
+ }
+
+ let code = '';
+ if (t.params.type.name.includes('f16')) {
+ code += 'enable f16;\n';
+ }
+
+ // Testing the struct case, generate the structf
+ if (t.params.type.name === 'S') {
+ code += `struct S {
+ a: mat4x2<f32>, // Align 8
+ b: array<vec${
+ t.params.address_space === 'storage' ? 2 : 4
+ }<i32>, 2>, // Storage align 8, uniform 16
+ }
+ `;
+ }
+
+ let align = t.params.align;
+ if (t.params.align === 'alignment') {
+ // Alignment value listed in the spec
+ if (t.params.address_space === 'storage') {
+ align = `${t.params.type.storage}`;
+ } else {
+ align = `${t.params.type.uniform}`;
+ }
+ }
+
+ let address_space = 'uniform';
+ if (t.params.address_space === 'storage') {
+ // atomics require read_write, not just the default of read
+ address_space = 'storage, read_write';
+ }
+
+ code += `struct MyStruct {
+ @align(${align}) a: ${t.params.type.name},
+ }
+
+ @group(0) @binding(0)
+ var<${address_space}> a : MyStruct;`;
+
+ code += `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(.4, .2, .3, .1);
+ }`;
+
+ const fails =
+ // An alignment of 1 is never valid as it is smaller then all required alignments.
+ t.params.align === 1 ||
+ // Except for f16, 2 should also fail as being too small.
+ (t.params.align === 2 && t.params.type.name !== 'f16') ||
+ // An array of `vec2` in uniform will not validate because, while the alignment on the array
+ // itself is fine, the `vec2` element inside the array will have the wrong alignment. Uniform
+ // requires that inner vec2 to have an align 16 which can only be done by specifying `vec4`
+ // instead.
+ (t.params.address_space === 'uniform' && t.params.type.name.startsWith('array<vec2'));
+
+ t.expectCompileResult(!fails, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/blankspace.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/blankspace.spec.ts
new file mode 100644
index 0000000000..e2a58520d5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/blankspace.spec.ts
@@ -0,0 +1,50 @@
+export const description = `Validation tests for blankspace handling`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('null_characters')
+ .desc(`Test that WGSL source containing a null character is rejected.`)
+ .params(u =>
+ u
+ .combine('contains_null', [true, false])
+ .combine('placement', ['comment', 'delimiter', 'eol'])
+ .beginSubcases()
+ )
+ .fn(t => {
+ let code = '';
+ if (t.params.placement === 'comment') {
+ code = `// Here is a ${t.params.contains_null ? '\0' : 'Z'} character`;
+ } else if (t.params.placement === 'delimiter') {
+ code = `const${t.params.contains_null ? '\0' : ' '}name : i32 = 0;`;
+ } else if (t.params.placement === 'eol') {
+ code = `const name : i32 = 0;${t.params.contains_null ? '\0' : ''}`;
+ }
+ t.expectCompileResult(!t.params.contains_null, code);
+ });
+
+g.test('blankspace')
+ .desc(`Test that all blankspace characters act as delimiters.`)
+ .params(u =>
+ u
+ .combine('blankspace', [
+ ['\u0020', 'space'],
+ ['\u0009', 'horizontal_tab'],
+ ['\u000a', 'line_feed'],
+ ['\u000b', 'vertical_tab'],
+ ['\u000c', 'form_feed'],
+ ['\u000d', 'carriage_return'],
+ ['\u0085', 'next_line'],
+ ['\u200e', 'left_to_right_mark'],
+ ['\u200f', 'right_to_left_mark'],
+ ['\u2028', 'line_separator'],
+ ['\u2029', 'paragraph_separator'],
+ ])
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = `const${t.params.blankspace[0]}ident : i32 = 0;`;
+ t.expectCompileResult(true, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/builtin.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/builtin.spec.ts
new file mode 100644
index 0000000000..3c684ee0fb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/builtin.spec.ts
@@ -0,0 +1,37 @@
+export const description = `Validation tests for @builtin`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kValidBuiltin = new Set([
+ `@builtin(position)`,
+ `@builtin(position,)`,
+ `@ \n builtin(position)`,
+ `@/^ comment ^/builtin/^ comment ^/\n\n(\t/^comment^/position/^comment^/)`,
+]);
+const kInvalidBuiltin = new Set([
+ `@abuiltin(position)`,
+ `@builtin`,
+ `@builtin()`,
+ `@builtin position`,
+ `@builtin position)`,
+ `@builtin(position`,
+ `@builtin(position, frag_depth)`,
+ `@builtin(identifier)`,
+ `@builtin(2)`,
+]);
+
+g.test('parse')
+ .desc(`Test that @builtin is parsed correctly.`)
+ .params(u => u.combine('builtin', new Set([...kValidBuiltin, ...kInvalidBuiltin])))
+ .fn(t => {
+ const v = t.params.builtin.replace(/\^/g, '*');
+ const code = `
+@vertex
+fn main() -> ${v} vec4<f32> {
+ return vec4<f32>(.4, .2, .3, .1);
+}`;
+ t.expectCompileResult(kValidBuiltin.has(t.params.builtin), code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/comments.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/comments.spec.ts
new file mode 100644
index 0000000000..af49c49619
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/comments.spec.ts
@@ -0,0 +1,75 @@
+export const description = `Validation tests for comments`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('comments')
+ .desc(`Test that valid comments are handled correctly, including nesting.`)
+ .fn(t => {
+ const code = `
+/**
+ * Here is my shader.
+ *
+ * /* I can nest /**/ comments. */
+ * // I can nest line comments too.
+ **/
+@fragment // This is the stage
+fn main(/*
+no
+parameters
+*/) -> @location(0) vec4<f32> {
+ return/*block_comments_delimit_tokens*/vec4<f32>(.4, .2, .3, .1);
+}/* terminated block comments are OK at EOF...*/`;
+ t.expectCompileResult(true, code);
+ });
+
+g.test('line_comment_eof')
+ .desc(`Test that line comments can come at EOF.`)
+ .fn(t => {
+ const code = `
+@fragment
+fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(.4, .2, .3, .1);
+}
+// line comments are OK at EOF...`;
+ t.expectCompileResult(true, code);
+ });
+
+g.test('line_comment_terminators')
+ .desc(`Test that line comments are terminated by any blankspace other than space and \t`)
+ .params(u =>
+ u
+ .combine('blankspace', [
+ [' ', 'space'],
+ ['\t', 'tab'],
+ ['\u000a', 'line_feed'],
+ ['\u000b', 'vertical_tab'],
+ ['\u000c', 'form_feed'],
+ ['\u000d', 'carriage_return'],
+ ['\u000d\u000a', 'carriage_return_line_feed'],
+ ['\u0085', 'next_line'],
+ ['\u2028', 'line_separator'],
+ ['\u2029', 'paragraph_separator'],
+ ])
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = `// Line comment${t.params.blankspace[0]}const invalid_outside_comment = should_fail`;
+
+ t.expectCompileResult([' ', '\t'].includes(t.params.blankspace[0]), code);
+ });
+
+g.test('unterminated_block_comment')
+ .desc(`Test that unterminated block comments cause an error`)
+ .params(u => u.combine('terminated', [true, false]).beginSubcases())
+ .fn(t => {
+ const code = `
+/**
+ * Unterminated block comment.
+ *
+ ${t.params.terminated ? '*/' : ''}`;
+
+ t.expectCompileResult(t.params.terminated, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/identifiers.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/identifiers.spec.ts
new file mode 100644
index 0000000000..6aefccc442
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/identifiers.spec.ts
@@ -0,0 +1,277 @@
+export const description = `Validation tests for identifiers`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kValidIdentifiers = new Set([
+ 'foo',
+ 'Foo',
+ 'FOO',
+ '_0',
+ '_foo0',
+ '_0foo',
+ 'foo__0',
+ 'Δέλτα',
+ 'réflexion',
+ 'Кызыл',
+ '𐰓𐰏𐰇',
+ '朝焼け',
+ 'سلام',
+ '검정',
+ 'שָׁלוֹם',
+ 'गुलाबी',
+ 'փիրուզ',
+]);
+const kInvalidIdentifiers = new Set([
+ '_', // Single underscore is a syntactic token for phony assignment.
+ '__', // Leading double underscore is reserved.
+ '__foo', // Leading double underscore is reserved.
+ '0foo', // Must start with single underscore or a letter.
+ // No punctuation:
+ 'foo.bar',
+ 'foo-bar',
+ 'foo+bar',
+ 'foo#bar',
+ 'foo!bar',
+ 'foo\\bar',
+ 'foo/bar',
+ 'foo,bar',
+ 'foo@bar',
+ 'foo::bar',
+ // Type-defining Keywords:
+ 'array',
+ 'atomic',
+ 'bool',
+ 'f32',
+ 'f16',
+ 'i32',
+ 'mat2x2',
+ 'mat2x3',
+ 'mat2x4',
+ 'mat3x2',
+ 'mat3x3',
+ 'mat3x4',
+ 'mat4x2',
+ 'mat4x3',
+ 'mat4x4',
+ 'ptr',
+ 'sampler',
+ 'sampler_comparison',
+ 'texture_1d',
+ 'texture_2d',
+ 'texture_2d_array',
+ 'texture_3d',
+ 'texture_cube',
+ 'texture_cube_array',
+ 'texture_multisampled_2d',
+ 'texture_storage_1d',
+ 'texture_storage_2d',
+ 'texture_storage_2d_array',
+ 'texture_storage_3d',
+ 'texture_depth_2d',
+ 'texture_depth_2d_array',
+ 'texture_depth_cube',
+ 'texture_depth_cube_array',
+ 'texture_depth_multisampled_2d',
+ 'u32',
+ 'vec2',
+ 'vec3',
+ 'vec4',
+ // Other Keywords:
+ 'bitcast',
+ 'break',
+ 'case',
+ 'const',
+ 'continue',
+ 'continuing',
+ 'default',
+ 'discard',
+ 'else',
+ 'enable',
+ 'false',
+ 'fn',
+ 'for',
+ 'if',
+ 'let',
+ 'loop',
+ 'override',
+ 'return',
+ 'static_assert',
+ 'struct',
+ 'switch',
+ 'true',
+ 'type',
+ 'var',
+ 'while',
+ // Reserved Words
+ 'CompileShader',
+ 'ComputeShader',
+ 'DomainShader',
+ 'GeometryShader',
+ 'Hullshader',
+ 'NULL',
+ 'Self',
+ 'abstract',
+ 'active',
+ 'alignas',
+ 'alignof',
+ 'as',
+ 'asm',
+ 'asm_fragment',
+ 'async',
+ 'attribute',
+ 'auto',
+ 'await',
+ 'become',
+ 'binding_array',
+ 'cast',
+ 'catch',
+ 'class',
+ 'co_await',
+ 'co_return',
+ 'co_yield',
+ 'coherent',
+ 'column_major',
+ 'common',
+ 'compile',
+ 'compile_fragment',
+ 'concept',
+ 'const_cast',
+ 'consteval',
+ 'constexpr',
+ 'constinit',
+ 'crate',
+ 'debugger',
+ 'decltype',
+ 'delete',
+ 'demote',
+ 'demote_to_helper',
+ 'do',
+ 'dynamic_cast',
+ 'enum',
+ 'explicit',
+ 'export',
+ 'extends',
+ 'extern',
+ 'external',
+ 'fallthrough',
+ 'filter',
+ 'final',
+ 'finally',
+ 'friend',
+ 'from',
+ 'fxgroup',
+ 'get',
+ 'goto',
+ 'groupshared',
+ 'handle',
+ 'highp',
+ 'impl',
+ 'implements',
+ 'import',
+ 'inline',
+ 'inout',
+ 'instanceof',
+ 'interface',
+ 'layout',
+ 'lowp',
+ 'macro',
+ 'macro_rules',
+ 'match',
+ 'mediump',
+ 'meta',
+ 'mod',
+ 'module',
+ 'move',
+ 'mut',
+ 'mutable',
+ 'namespace',
+ 'new',
+ 'nil',
+ 'noexcept',
+ 'noinline',
+ 'nointerpolation',
+ 'noperspective',
+ 'null',
+ 'nullptr',
+ 'of',
+ 'operator',
+ 'package',
+ 'packoffset',
+ 'partition',
+ 'pass',
+ 'patch',
+ 'pixelfragment',
+ 'precise',
+ 'precision',
+ 'premerge',
+ 'priv',
+ 'protected',
+ 'pub',
+ 'public',
+ 'readonly',
+ 'ref',
+ 'regardless',
+ 'register',
+ 'reinterpret_cast',
+ 'requires',
+ 'resource',
+ 'restrict',
+ 'self',
+ 'set',
+ 'shared',
+ 'signed',
+ 'sizeof',
+ 'smooth',
+ 'snorm',
+ 'static',
+ 'static_cast',
+ 'std',
+ 'subroutine',
+ 'super',
+ 'target',
+ 'template',
+ 'this',
+ 'thread_local',
+ 'throw',
+ 'trait',
+ 'try',
+ 'typedef',
+ 'typeid',
+ 'typename',
+ 'typeof',
+ 'union',
+ 'unless',
+ 'unorm',
+ 'unsafe',
+ 'unsized',
+ 'use',
+ 'using',
+ 'varying',
+ 'virtual',
+ 'volatile',
+ 'wgsl',
+ 'where',
+ 'with',
+ 'writeonly',
+ 'yield',
+]);
+g.test('identifiers')
+ .desc(`Test that valid identifiers are accepted, and invalid identifiers are rejected.`)
+ .params(u =>
+ u.combine('ident', new Set([...kValidIdentifiers, ...kInvalidIdentifiers])).beginSubcases()
+ )
+ .fn(t => {
+ const code = `var<private> ${t.params.ident} : i32;`;
+ t.expectCompileResult(kValidIdentifiers.has(t.params.ident), code);
+ });
+
+g.test('non_normalized')
+ .desc(`Test that identifiers are not unicode normalized`)
+ .fn(t => {
+ const code = `var<private> \u212b : i32; // \u212b normalizes with NFC to \u00c5
+var<private> \u00c5 : i32;`;
+ t.expectCompileResult(true, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/literal.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/literal.spec.ts
new file mode 100644
index 0000000000..0c5c820040
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/literal.spec.ts
@@ -0,0 +1,296 @@
+export const description = `Validation tests for literals`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('bools')
+ .desc(`Test that valid bools are accepted.`)
+ .params(u => u.combine('val', ['true', 'false']).beginSubcases())
+ .fn(t => {
+ const code = `var test = ${t.params.val};`;
+ t.expectCompileResult(true, t.wrapInEntryPoint(code));
+ });
+
+const kAbstractIntNonNegative = new Set([
+ '0x123', // hex number
+ '123', // signed number, no suffix
+ '0', // zero
+ '0x3f', // hex with 'f' as last character
+ '2147483647', // max signed int
+]);
+
+const kAbstractIntNegative = new Set([
+ '-0x123', // hex number
+ '-123', // signed number, no suffix
+ '-0x3f', // hex with 'f' as last character
+ '-2147483647', // nagative of max signed int
+ '-2147483648', // min signed int
+]);
+
+const kI32 = new Set([
+ '94i', // signed number
+ '2147483647i', // max signed int
+ '-2147483647i', // min parsable signed int
+ 'i32(-2147483648)', // min signed int
+]);
+
+const kU32 = new Set([
+ '42u', // unsigned number
+ '0u', // min unsigned int
+ '4294967295u', // max unsigned int
+]);
+
+{
+ const kValidIntegers = new Set([
+ ...kAbstractIntNonNegative,
+ ...kAbstractIntNegative,
+ ...kI32,
+ ...kU32,
+ ]);
+ const kInvalidIntegers = new Set([
+ '0123', // Integer does not start with zero
+ '2147483648i', // max signed int + 1
+ '-2147483649i', // min signed int - 1
+ '4294967295', // a untyped lhs will be i32, so this is too big
+ '4294967295i', // max unsigned int with i suffix
+ '4294967296u', // max unsigned int + 1
+ '-1u', // negative unsigned
+ ]);
+ g.test('abstract_int')
+ .desc(`Test that valid integers are accepted, and invalid integers are rejected.`)
+ .params(u =>
+ u.combine('val', new Set([...kValidIntegers, ...kInvalidIntegers])).beginSubcases()
+ )
+ .fn(t => {
+ const code = `var test = ${t.params.val};`;
+ t.expectCompileResult(kValidIntegers.has(t.params.val), t.wrapInEntryPoint(code));
+ });
+}
+
+{
+ const kValidI32 = new Set([...kAbstractIntNonNegative, ...kAbstractIntNegative, ...kI32]);
+ const kInvalidI32 = new Set([
+ ...kU32,
+ '2147483648', // max signed int + 1
+ '2147483648i', // max signed int + 1
+ '-2147483649', // min signed int - 1
+ '-2147483649i', // min signed int - 1
+ '1.0', // no conversion from float
+ '1.0f', // no conversion from float
+ '1.0h', // no conversion from float
+ ]);
+ g.test('i32')
+ .desc(`Test that valid signed integers are accepted, and invalid signed integers are rejected.`)
+ .params(u => u.combine('val', new Set([...kValidI32, ...kInvalidI32])).beginSubcases())
+ .beforeAllSubcases(t => {
+ if (t.params.val.includes('h')) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ const { val } = t.params;
+ const code = `var test: i32 = ${val};`;
+ const extensionList = val.includes('h') ? ['f16'] : [];
+ t.expectCompileResult(kValidI32.has(val), t.wrapInEntryPoint(code, extensionList));
+ });
+}
+
+{
+ const kValidU32 = new Set([
+ ...kAbstractIntNonNegative,
+ ...kU32,
+ '4294967295', // max unsigned
+ ]);
+ const kInvalidU32 = new Set([
+ ...kAbstractIntNegative,
+ ...kI32,
+ '4294967296', // max unsigned int + 1
+ '4294967296u', // min unsigned int + 1
+ '-1', // min unsigned int - 1
+ '1.0', // no conversion from float
+ '1.0f', // no conversion from float
+ '1.0h', // no conversion from float
+ ]);
+ g.test('u32')
+ .desc(
+ `Test that valid unsigned integers are accepted, and invalid unsigned integers are rejected.`
+ )
+ .params(u => u.combine('val', new Set([...kValidU32, ...kInvalidU32])).beginSubcases())
+ .beforeAllSubcases(t => {
+ if (t.params.val.includes('h')) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ const { val } = t.params;
+ const code = `var test: u32 = ${val};`;
+ const extensionList = val.includes('h') ? ['f16'] : [];
+ t.expectCompileResult(kValidU32.has(val), t.wrapInEntryPoint(code, extensionList));
+ });
+}
+
+const kF32 = new Set([
+ '0f', // Zero float
+ '0.0f', // Zero float
+ '12.223f', // float value
+ '12.f', // .f
+ '.12f', // No leading number with a f
+ '2.4e+4f', // Positive exponent with f suffix
+ '2.4e-2f', // Negative exponent with f suffix
+ '2.e+4f', // Exponent without decimals
+ '1e-4f', // Exponennt without decimal point
+ '0x1P+4f', // Hex float no decimal
+]);
+
+const kF16 = new Set([
+ '0h', // Zero half
+ '1h', // Half no decimal
+ '.1h', // Half no leading value
+ '1.1e2h', // Exponent half no sign
+ '1.1E+2h', // Exponent half, plus (uppercase E)
+ '2.4e-2h', // Exponent half, negative
+ '0xep2h', // Hexfloat half lower case p
+ '0xEp-2h', // Hexfloat uppcase hex value
+ '0x3p+2h', // Hex float half positive exponent
+ '0x3.2p+2h', // Hex float with decimal half
+]);
+
+const kAbstractFloat = new Set([
+ '0.0', // Zero float without suffix
+ '.0', // Zero float without leading value
+ '12.', // No decimal points
+ '00012.', // Leading zeros allowed
+ '.12', // No leading digits
+ '1.2e2', // Exponent without sign (lowercase e)
+ '1.2E2', // Exponent without sign (uppercase e)
+ '1.2e+2', // positive exponent
+ '2.4e-2', // Negative exponent
+ '.1e-2', // Exponent without leading number
+ '0x.3', // Hex float, lowercase X
+ '0X.3', // Hex float, uppercase X
+ '0xa.fp+2', // Hex float, lowercase p
+ '0xa.fP+2', // Hex float, uppercase p
+ '0xE.fp+2', // Uppercase E (as hex, but matches non hex exponent char)
+ '0X1.fp-4', // Hex float negative exponent
+]);
+
+{
+ const kValidFloats = new Set([...kF32, ...kF16, ...kAbstractFloat]);
+ const kInvalidFloats = new Set([
+ '.f', // Must have a number
+ '.e-2', // Exponent without leading values
+ '1.e&2f', // Exponent invalid sign
+ '1.ef', // Exponent without value
+ '1.e+f', // Exponent sign no value
+ '0x.p2', // Hex float no value
+ '0x1p', // Hex float missing exponent
+ '0x1p^', // Hex float invalid exponent
+ '1.0e+999999999999f', // Too big
+ '0x1.0p+999999999999f', // Too big hex
+ '0x1.00000001pf0', // Mantissa too big
+ ]);
+ const kInvalidF16s = new Set([
+ '1.1eh', // Missing exponent value
+ '1.1e%2h', // Invalid exponent sign
+ '1.1e+h', // Missing exponent with sign
+ '1.0e+999999h', // Too large
+ '0x1.0p+999999h', // Too large hex
+ '0xf.h', // Having suffix "h" without "p" or "P"
+ '0x3h', // Having suffix "h" without "p" or "P"
+ ]);
+
+ g.test('abstract_float')
+ .desc(`Test that valid floats are accepted, and invalid floats are rejected`)
+ .params(u =>
+ u
+ .combine('val', new Set([...kValidFloats, ...kInvalidFloats, ...kInvalidF16s]))
+ .beginSubcases()
+ )
+ .beforeAllSubcases(t => {
+ if (kF16.has(t.params.val) || kInvalidF16s.has(t.params.val)) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ const code = `var test = ${t.params.val};`;
+ const extensionList = kF16.has(t.params.val) || kInvalidF16s.has(t.params.val) ? ['f16'] : [];
+ t.expectCompileResult(
+ kValidFloats.has(t.params.val),
+ t.wrapInEntryPoint(code, extensionList)
+ );
+ });
+}
+
+{
+ const kValidF32 = new Set([
+ ...kF32,
+ ...kAbstractFloat,
+ '1', // AbstractInt
+ '-1', // AbstractInt
+ ]);
+ const kInvalidF32 = new Set([
+ ...kF16, // no conversion
+ '1u', // unsigned
+ '1i', // signed
+ '1h', // half float
+ '.f', // Must have a number
+ '.e-2', // Exponent without leading values
+ '1.e&2f', // Exponent invalid sign
+ '1.ef', // Exponent without value
+ '1.e+f', // Exponent sign no value
+ '0x.p2', // Hex float no value
+ '0x1p', // Hex float missing exponent
+ '0x1p^', // Hex float invalid exponent
+ '1.0e+999999999999f', // Too big
+ '0x1.0p+999999999999f', // Too big hex
+ '0x1.00000001pf0', // Mantissa too big
+ ]);
+
+ g.test('f32')
+ .desc(`Test that valid floats are accepted, and invalid floats are rejected`)
+ .params(u => u.combine('val', new Set([...kValidF32, ...kInvalidF32])).beginSubcases())
+ .beforeAllSubcases(t => {
+ if (kF16.has(t.params.val)) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ const { val } = t.params;
+ const code = `var test: f32 = ${val};`;
+ const extensionList = kF16.has(val) ? ['f16'] : [];
+ t.expectCompileResult(kValidF32.has(val), t.wrapInEntryPoint(code, extensionList));
+ });
+}
+
+{
+ const kValidF16 = new Set([
+ ...kF16,
+ ...kAbstractFloat,
+ '1', // AbstractInt
+ '-1', // AbstractInt
+ ]);
+ const kInvalidF16 = new Set([
+ ...kF32,
+ '1i', // signed int
+ '1u', // unsigned int
+ '1f', // no conversion from f32 to f16
+ '1.1eh', // Missing exponent value
+ '1.1e%2h', // Invalid exponent sign
+ '1.1e+h', // Missing exponent with sign
+ '1.0e+999999h', // Too large
+ '0x1.0p+999999h', // Too large hex
+ ]);
+
+ g.test('f16')
+ .desc(
+ `
+Test that valid half floats are accepted, and invalid half floats are rejected
+
+TODO: Need to inject the 'enable fp16' into the shader to enable the parsing.
+`
+ )
+ .params(u => u.combine('val', new Set([...kValidF16, ...kInvalidF16])).beginSubcases())
+ .unimplemented();
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts
new file mode 100644
index 0000000000..8414fef45f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/semicolon.spec.ts
@@ -0,0 +1,269 @@
+export const description = `Validation tests for semicolon placements`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('module_scope_single')
+ .desc(`Test that a semicolon can be placed at module scope.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `;`);
+ });
+
+g.test('module_scope_multiple')
+ .desc(`Test that multiple semicolons can be placed at module scope.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `;;;`);
+ });
+
+g.test('after_enable')
+ .desc(`Test that a semicolon must be placed after an enable directive.`)
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase({ requiredFeatures: ['shader-f16'] });
+ })
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `enable f16;`);
+ t.expectCompileResult(/* pass */ false, `enable f16`);
+ });
+
+g.test('after_struct_decl')
+ .desc(`Test that a semicolon can be placed after an struct declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `struct S { x : i32 };`);
+ t.expectCompileResult(/* pass */ true, `struct S { x : i32 }`);
+ });
+
+g.test('after_member')
+ .desc(`Test that a semicolon must not be placed after an struct member declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `struct S { x : i32 }`);
+ t.expectCompileResult(/* pass */ false, `struct S { x : i32; }`);
+ });
+
+g.test('after_func_decl')
+ .desc(`Test that a semicolon can be placed after a function declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() {};`);
+ t.expectCompileResult(/* pass */ true, `fn f() {}`);
+ });
+
+g.test('after_type_alias_decl')
+ .desc(`Test that a semicolon must be placed after an type alias declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `type T = i32;`);
+ t.expectCompileResult(/* pass */ false, `type T = i32`);
+ });
+
+g.test('after_return')
+ .desc(`Test that a semicolon must be placed after a return statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { return; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { return }`);
+ });
+
+g.test('after_call')
+ .desc(`Test that a semicolon must be placed after a function call.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { workgroupBarrier(); }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { workgroupBarrier() }`);
+ });
+
+g.test('after_module_const_decl')
+ .desc(`Test that a semicolon must be placed after a module-scope const declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `const v = 1;`);
+ t.expectCompileResult(/* pass */ false, `const v = 1`);
+ });
+
+g.test('after_fn_const_decl')
+ .desc(`Test that a semicolon must be placed after a function-scope const declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { const v = 1; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { const v = 1 }`);
+ });
+
+g.test('after_module_var_decl')
+ .desc(`Test that a semicolon must be placed after a module-scope var declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `var<private> v = 1;`);
+ t.expectCompileResult(/* pass */ false, `var<private> v = 1`);
+ });
+
+g.test('after_fn_var_decl')
+ .desc(`Test that a semicolon must be placed after a function-scope var declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { var v = 1; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { var v = 1 }`);
+ });
+
+g.test('after_let_decl')
+ .desc(`Test that a semicolon must be placed after a let declaration.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { let v = 1; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { let v = 1 }`);
+ });
+
+g.test('after_discard')
+ .desc(`Test that a semicolon must be placed after a discard statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { discard; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { discard }`);
+ });
+
+g.test('after_assignment')
+ .desc(`Test that a semicolon must be placed after an assignment statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { var v = 1; v = 2; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { var v = 1; v = 2 }`);
+ });
+
+g.test('after_fn_static_assert')
+ .desc(`Test that a semicolon must be placed after an function-scope static assert.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { static_assert(true); }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { static_assert(true) }`);
+ });
+
+g.test('function_body_single')
+ .desc(`Test that a semicolon can be placed in a function body.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { ; }`);
+ });
+
+g.test('function_body_multiple')
+ .desc(`Test that multiple semicolons can be placed in a function body.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { ;;; }`);
+ });
+
+g.test('compound_statement_single')
+ .desc(`Test that a semicolon can be placed in a compound statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { { ; } }`);
+ });
+
+g.test('compound_statement_multiple')
+ .desc(`Test that multiple semicolons can be placed in a compound statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { { ;;; } }`);
+ });
+
+g.test('after_compound_statement')
+ .desc(`Test that a semicolon can be placed after a compound statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { {} ; }`);
+ });
+
+g.test('after_if')
+ .desc(`Test that a semicolon can be placed after an if-statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { if true {} ; }`);
+ });
+
+g.test('after_if_else')
+ .desc(`Test that a semicolon can be placed after an if-else-statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { if true {} else {} ; }`);
+ });
+
+g.test('after_switch')
+ .desc(`Test that a semicolon can be placed after an switch-statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { switch 1 { default {} } ; }`);
+ });
+
+g.test('after_case')
+ .desc(`Test that a semicolon cannot be placed after a non-default switch case.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ false, `fn f() { switch 1 { case 1 {}; default {} } }`);
+ t.expectCompileResult(/* pass */ true, `fn f() { switch 1 { case 1 {} default {} } }`);
+ });
+
+g.test('after_case_break')
+ .desc(`Test that a semicolon must be placed after a case break statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ false, `fn f() { switch 1 { case 1 { break } default {} } }`);
+ t.expectCompileResult(/* pass */ true, `fn f() { switch 1 { case 1 { break; } default {} } }`);
+ });
+
+g.test('after_default_case')
+ .desc(`Test that a semicolon cannot be placed after a default switch case.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ false, `fn f() { switch 1 { default {}; } }`);
+ t.expectCompileResult(/* pass */ true, `fn f() { switch 1 { default {} } }`);
+ });
+
+g.test('after_default_case_break')
+ .desc(`Test that a semicolon cannot be placed after a default switch case.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ false, `fn f() { switch 1 { default { break } } }`);
+ t.expectCompileResult(/* pass */ true, `fn f() { switch 1 { default { break; } } }`);
+ });
+
+g.test('after_for')
+ .desc(`Test that a semicolon can be placed after a for-loop.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { for (; false;) {}; }`);
+ });
+
+g.test('after_for_break')
+ .desc(`Test that a semicolon must be placed after a for-loop break statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { for (; false;) { break; } }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { for (; false;) { break } }`);
+ });
+
+g.test('after_loop')
+ .desc(`Test that a semicolon can be placed after a loop.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { loop { break; }; }`);
+ });
+
+g.test('after_loop_break')
+ .desc(`Test that a semicolon must be placed after a loop break statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { loop { break; }; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { loop { break }; }`);
+ });
+
+g.test('after_loop_break_if')
+ .desc(`Test that a semicolon must be placed after a loop break-if statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { loop { continuing { break if true; } }; }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { loop { continuing { break if true } }; }`);
+ });
+
+g.test('after_loop_continue')
+ .desc(`Test that a semicolon must be placed after a loop continue statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { loop { if true { continue; } { break; } } }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { loop { if true { continue } { break; } } }`);
+ });
+
+g.test('after_continuing')
+ .desc(`Test that a semicolon cannot be placed after a continuing.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ false, `fn f() { loop { break; continuing{}; } }`);
+ t.expectCompileResult(/* pass */ true, `fn f() { loop { break; continuing{} } }`);
+ });
+
+g.test('after_while')
+ .desc(`Test that a semicolon cannot be placed after a while-loop.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { while false {}; }`);
+ });
+
+g.test('after_while_break')
+ .desc(`Test that a semicolon must be placed after a while break statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { while false { break; } }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { while false { break } }`);
+ });
+
+g.test('after_while_continue')
+ .desc(`Test that a semicolon must be placed after a while continue statement.`)
+ .fn(t => {
+ t.expectCompileResult(/* pass */ true, `fn f() { while false { continue; } }`);
+ t.expectCompileResult(/* pass */ false, `fn f() { while false { continue } }`);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/source.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/source.spec.ts
new file mode 100644
index 0000000000..40da5d2baf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/source.spec.ts
@@ -0,0 +1,29 @@
+export const description = `Validation tests for source parsing`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('valid_source')
+ .desc(`Tests that a valid source is consumed successfully.`)
+ .fn(t => {
+ const code = `
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(.4, .2, .3, .1);
+ }`;
+ t.expectCompileResult(true, code);
+ });
+
+g.test('empty')
+ .desc(`Test that an empty source is consumed successfully.`)
+ .fn(t => {
+ t.expectCompileResult(true, '');
+ });
+
+g.test('invalid_source')
+ .desc(`Tests that a source which does not match the grammar fails.`)
+ .fn(t => {
+ t.expectCompileResult(false, 'invalid_source');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/static_assert.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/static_assert.spec.ts
new file mode 100644
index 0000000000..effc6e458d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/static_assert.spec.ts
@@ -0,0 +1,37 @@
+export const description = `Parser validation tests for static_assert`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kCases = {
+ no_parentheses: { code: `static_assert true;`, pass: true },
+ left_parenthesis_only: { code: `static_assert(true;`, pass: false },
+ right_parenthesis_only: { code: `static_assert true);`, pass: false },
+ both_parentheses: { code: `static_assert(true);`, pass: true },
+ condition_on_newline: {
+ code: `static_assert
+true;`,
+ pass: true,
+ },
+ multiline_with_parentheses: {
+ code: `static_assert
+(
+ true
+);`,
+ pass: true,
+ },
+ invalid_expression: { code: `static_assert(1!2);`, pass: false },
+ no_condition_no_parentheses: { code: `static_assert;`, pass: false },
+ no_condition_with_parentheses: { code: `static_assert();`, pass: false },
+ not_a_boolean: { code: `static_assert 42;`, pass: false },
+};
+
+g.test('parse')
+ .desc(`Tests that the static_assert statement parses correctly.`)
+ .params(u => u.combine('case', Object.keys(kCases) as Array<keyof typeof kCases>).beginSubcases())
+ .fn(t => {
+ const c = kCases[t.params.case];
+ t.expectCompileResult(c.pass, c.code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/var_and_let.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/var_and_let.spec.ts
new file mode 100644
index 0000000000..d8f76b8235
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/parse/var_and_let.spec.ts
@@ -0,0 +1,72 @@
+export const description = `
+Positive and negative validation tests for variable and const.
+
+TODO: Find a better way to test arrays than using a single arbitrary size. [1]
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kTestTypes = [
+ 'f32',
+ 'i32',
+ 'u32',
+ 'bool',
+ 'vec2<f32>',
+ 'vec2<i32>',
+ 'vec2<u32>',
+ 'vec2<bool>',
+ 'vec3<f32>',
+ 'vec3<i32>',
+ 'vec3<u32>',
+ 'vec3<bool>',
+ 'vec4<f32>',
+ 'vec4<i32>',
+ 'vec4<u32>',
+ 'vec4<bool>',
+ 'mat2x2<f32>',
+ 'mat2x3<f32>',
+ 'mat2x4<f32>',
+ 'mat3x2<f32>',
+ 'mat3x3<f32>',
+ 'mat3x4<f32>',
+ 'mat4x2<f32>',
+ 'mat4x3<f32>',
+ 'mat4x4<f32>',
+ // [1]: 12 is a random number here. find a solution to replace it.
+ 'array<f32, 12>',
+ 'array<i32, 12>',
+ 'array<u32, 12>',
+ 'array<bool, 12>',
+] as const;
+
+g.test('initializer_type')
+ .desc(
+ `
+ If present, the initializer's type must match the store type of the variable.
+ Testing scalars, vectors, and matrices of every dimension and type.
+ TODO: add test for: structs - arrays of vectors and matrices - arrays of different length
+`
+ )
+ .params(u =>
+ u
+ .combine('variableOrConstant', ['var', 'let'])
+ .beginSubcases()
+ .combine('lhsType', kTestTypes)
+ .combine('rhsType', kTestTypes)
+ )
+ .fn(t => {
+ const { variableOrConstant, lhsType, rhsType } = t.params;
+
+ const code = `
+ @fragment
+ fn main() {
+ ${variableOrConstant} a : ${lhsType} = ${rhsType}();
+ }
+ `;
+
+ const expectation = lhsType === rhsType;
+ t.expectCompileResult(expectation, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/bindings.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/bindings.spec.ts
new file mode 100644
index 0000000000..1b6786843a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/bindings.spec.ts
@@ -0,0 +1,118 @@
+export const description = `Validation tests for resource interface bindings`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+import {
+ declareEntrypoint,
+ kResourceEmitters,
+ kResourceKindsA,
+ kResourceKindsB,
+ ResourceDeclarationEmitter,
+} from './util.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('single_entry_point')
+ .desc(
+ `Test that two different resource variables in a shader must not have the same group and binding values, when considered as a pair.`
+ )
+ .params(u =>
+ u
+ .combine('stage', ['vertex', 'fragment', 'compute'] as const)
+ .combine('a_kind', kResourceKindsA)
+ .combine('b_kind', kResourceKindsB)
+ .combine('a_group', [0, 3] as const)
+ .combine('b_group', [0, 3] as const)
+ .combine('a_binding', [0, 3] as const)
+ .combine('b_binding', [0, 3] as const)
+ .combine('usage', ['direct', 'transitive'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const resourceA = kResourceEmitters.get(t.params.a_kind) as ResourceDeclarationEmitter;
+ const resourceB = kResourceEmitters.get(t.params.b_kind) as ResourceDeclarationEmitter;
+ const resources = `
+${resourceA('resource_a', t.params.a_group, t.params.a_binding)}
+${resourceB('resource_b', t.params.b_group, t.params.b_binding)}
+`;
+ const expect =
+ t.params.a_group !== t.params.b_group || t.params.a_binding !== t.params.b_binding;
+
+ if (t.params.usage === 'direct') {
+ const code = `
+${resources}
+${declareEntrypoint('main', t.params.stage, '_ = resource_a; _ = resource_b;')}
+`;
+ t.expectCompileResult(expect, code);
+ } else {
+ const code = `
+${resources}
+fn use_a() { _ = resource_a; }
+fn use_b() { _ = resource_b; }
+${declareEntrypoint('main', t.params.stage, 'use_a(); use_b();')}
+`;
+ t.expectCompileResult(expect, code);
+ }
+ });
+
+g.test('different_entry_points')
+ .desc(
+ `Test that resources may use the same binding points if exclusively accessed by different entry points.`
+ )
+ .params(u =>
+ u
+ .combine('a_stage', ['vertex', 'fragment', 'compute'] as const)
+ .combine('b_stage', ['vertex', 'fragment', 'compute'] as const)
+ .combine('a_kind', kResourceKindsA)
+ .combine('b_kind', kResourceKindsB)
+ .combine('usage', ['direct', 'transitive'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const resourceA = kResourceEmitters.get(t.params.a_kind) as ResourceDeclarationEmitter;
+ const resourceB = kResourceEmitters.get(t.params.b_kind) as ResourceDeclarationEmitter;
+ const resources = `
+${resourceA('resource_a', /* group */ 0, /* binding */ 0)}
+${resourceB('resource_b', /* group */ 0, /* binding */ 0)}
+`;
+ const expect = true; // Binding reuse between different entry points is fine.
+
+ if (t.params.usage === 'direct') {
+ const code = `
+${resources}
+${declareEntrypoint('main_a', t.params.a_stage, '_ = resource_a;')}
+${declareEntrypoint('main_b', t.params.b_stage, '_ = resource_b;')}
+`;
+ t.expectCompileResult(expect, code);
+ } else {
+ const code = `
+${resources}
+fn use_a() { _ = resource_a; }
+fn use_b() { _ = resource_b; }
+${declareEntrypoint('main_a', t.params.a_stage, 'use_a();')}
+${declareEntrypoint('main_b', t.params.b_stage, 'use_b();')}
+`;
+ t.expectCompileResult(expect, code);
+ }
+ });
+
+g.test('binding_attributes')
+ .desc(`Test that both @group and @binding attributes must both be declared.`)
+ .params(u =>
+ u
+ .combine('stage', ['vertex', 'fragment', 'compute'] as const)
+ .combine('has_group', [true, false] as const)
+ .combine('has_binding', [true, false] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const emitter = kResourceEmitters.get('uniform') as ResourceDeclarationEmitter;
+ const code = emitter(
+ 'R',
+ t.params.has_group ? 0 : undefined,
+ t.params.has_binding ? 0 : undefined
+ );
+ const expect = t.params.has_group && t.params.has_binding;
+ t.expectCompileResult(expect, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/util.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/util.ts
new file mode 100644
index 0000000000..a4d0889932
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/resource_interface/util.ts
@@ -0,0 +1,91 @@
+/**
+ * ResourceDeclarationEmitter is a function that emits the WGSL declaring a resource variable with
+ * the given group, binding and name.
+ */
+export type ResourceDeclarationEmitter = (name: string, group?: number, binding?: number) => string;
+
+/** Helper function for emitting a resource declaration's group and binding attributes */
+function groupAndBinding(group?: number, binding?: number): string {
+ return (
+ `${group !== undefined ? `@group(${group})` : '/* no group */'} ` +
+ `${binding !== undefined ? `@binding(${binding})` : '/* no binding */'}`
+ );
+}
+
+/** Helper function for emitting a resource declaration for the given type */
+function basicEmitter(type: string): ResourceDeclarationEmitter {
+ return (name: string, group?: number, binding?: number) =>
+ `${groupAndBinding(group, binding)} var ${name} : ${type};\n`;
+}
+
+/** Map of resource declaration name, to an emitter. */
+export const kResourceEmitters = new Map<string, ResourceDeclarationEmitter>([
+ ['texture_1d', basicEmitter('texture_1d<i32>')],
+ ['texture_2d', basicEmitter('texture_2d<i32>')],
+ ['texture_2d_array', basicEmitter('texture_2d_array<f32>')],
+ ['texture_3d', basicEmitter('texture_3d<i32>')],
+ ['texture_cube', basicEmitter('texture_cube<u32>')],
+ ['texture_cube_array', basicEmitter('texture_cube_array<u32>')],
+ ['texture_multisampled_2d', basicEmitter('texture_multisampled_2d<i32>')],
+ ['texture_external', basicEmitter('texture_external')],
+ ['texture_storage_1d', basicEmitter('texture_storage_1d<rgba8unorm, write>')],
+ ['texture_storage_2d', basicEmitter('texture_storage_2d<rgba8sint, write>')],
+ ['texture_storage_2d_array', basicEmitter('texture_storage_2d_array<r32uint, write>')],
+ ['texture_storage_3d', basicEmitter('texture_storage_3d<rg32uint, write>')],
+ ['texture_depth_2d', basicEmitter('texture_depth_2d')],
+ ['texture_depth_2d_array', basicEmitter('texture_depth_2d_array')],
+ ['texture_depth_cube', basicEmitter('texture_depth_cube')],
+ ['texture_depth_cube_array', basicEmitter('texture_depth_cube_array')],
+ ['texture_depth_multisampled_2d', basicEmitter('texture_depth_multisampled_2d')],
+ ['sampler', basicEmitter('sampler')],
+ ['sampler_comparison', basicEmitter('sampler_comparison')],
+ [
+ 'uniform',
+ (name: string, group?: number, binding?: number) =>
+ `${groupAndBinding(group, binding)} var<uniform> ${name} : array<vec4<f32>, 16>;\n`,
+ ],
+ [
+ 'storage',
+ (name: string, group?: number, binding?: number) =>
+ `${groupAndBinding(group, binding)} var<storage> ${name} : array<vec4<f32>, 16>;\n`,
+ ],
+]);
+
+/** A small selection of resource declaration names, which can be used in test permutations */
+export const kResourceKindsA = ['storage', 'texture_2d', 'texture_external', 'uniform'];
+
+/** A small selection of resource declaration names, which can be used in test permutations */
+export const kResourceKindsB = ['texture_3d', 'texture_storage_1d', 'uniform'];
+
+/** An enumerator of shader stages */
+export type ShaderStage = 'vertex' | 'fragment' | 'compute';
+
+/**
+ * declareEntrypoint emits the WGSL to declare an entry point with the given name, stage and body.
+ * The generated function will have an appropriate return type and return statement, so that @p body
+ * does not have to change between stage.
+ * @param name the entry point function name
+ * @param stage the entry point stage
+ * @param body the body of the function (excluding any automatically suffixed return statements)
+ * @returns the WGSL string for the entry point
+ */
+export function declareEntrypoint(name: string, stage: ShaderStage, body: string): string {
+ switch (stage) {
+ case 'vertex':
+ return `@vertex
+fn ${name}() -> @builtin(position) vec4f {
+ ${body}
+ return vec4f();
+}`;
+ case 'fragment':
+ return `@fragment
+fn ${name}() {
+ ${body}
+}`;
+ case 'compute':
+ return `@compute @workgroup_size(1)
+fn ${name}() {
+ ${body}
+}`;
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts
new file mode 100644
index 0000000000..e57677a243
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/builtins.spec.ts
@@ -0,0 +1,277 @@
+export const description = `Validation tests for entry point built-in variables`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+import { generateShader } from './util.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+// List of all built-in variables and their stage, in|out usage, and type.
+// Taken from table in Section 15:
+// https://www.w3.org/TR/2021/WD-WGSL-20211013/#builtin-variables
+export const kBuiltins = [
+ { name: 'vertex_index', stage: 'vertex', io: 'in', type: 'u32' },
+ { name: 'instance_index', stage: 'vertex', io: 'in', type: 'u32' },
+ { name: 'position', stage: 'vertex', io: 'out', type: 'vec4<f32>' },
+ { name: 'position', stage: 'fragment', io: 'in', type: 'vec4<f32>' },
+ { name: 'front_facing', stage: 'fragment', io: 'in', type: 'bool' },
+ { name: 'frag_depth', stage: 'fragment', io: 'out', type: 'f32' },
+ { name: 'local_invocation_id', stage: 'compute', io: 'in', type: 'vec3<u32>' },
+ { name: 'local_invocation_index', stage: 'compute', io: 'in', type: 'u32' },
+ { name: 'global_invocation_id', stage: 'compute', io: 'in', type: 'vec3<u32>' },
+ { name: 'workgroup_id', stage: 'compute', io: 'in', type: 'vec3<u32>' },
+ { name: 'num_workgroups', stage: 'compute', io: 'in', type: 'vec3<u32>' },
+ { name: 'sample_index', stage: 'fragment', io: 'in', type: 'u32' },
+ { name: 'sample_mask', stage: 'fragment', io: 'in', type: 'u32' },
+ { name: 'sample_mask', stage: 'fragment', io: 'out', type: 'u32' },
+] as const;
+
+// List of types to test against.
+const kTestTypes = [
+ 'bool',
+ 'u32',
+ 'i32',
+ 'f32',
+ 'vec2<bool>',
+ 'vec2<u32>',
+ 'vec2<i32>',
+ 'vec2<f32>',
+ 'vec3<bool>',
+ 'vec3<u32>',
+ 'vec3<i32>',
+ 'vec3<f32>',
+ 'vec4<bool>',
+ 'vec4<u32>',
+ 'vec4<i32>',
+ 'vec4<f32>',
+ 'mat2x2<f32>',
+ 'mat2x3<f32>',
+ 'mat2x4<f32>',
+ 'mat3x2<f32>',
+ 'mat3x3<f32>',
+ 'mat3x4<f32>',
+ 'mat4x2<f32>',
+ 'mat4x3<f32>',
+ 'mat4x4<f32>',
+ 'atomic<u32>',
+ 'atomic<i32>',
+ 'array<bool,4>',
+ 'array<u32,4>',
+ 'array<i32,4>',
+ 'array<f32,4>',
+ 'MyStruct',
+] as const;
+
+g.test('stage_inout')
+ .desc(
+ `Test that each @builtin attribute is validated against the required stage and in/out usage for that built-in variable.`
+ )
+ .params(u =>
+ u
+ .combineWithParams(kBuiltins)
+ .combine('use_struct', [true, false] as const)
+ .combine('target_stage', ['', 'vertex', 'fragment', 'compute'] as const)
+ .combine('target_io', ['in', 'out'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = generateShader({
+ attribute: `@builtin(${t.params.name})`,
+ type: t.params.type,
+ stage: t.params.target_stage,
+ io: t.params.target_io,
+ use_struct: t.params.use_struct,
+ });
+
+ // Expect to pass iff the built-in table contains an entry that matches.
+ const expectation = kBuiltins.some(
+ x =>
+ x.name === t.params.name &&
+ (x.stage === t.params.target_stage ||
+ (t.params.use_struct && t.params.target_stage === '')) &&
+ (x.io === t.params.target_io || t.params.target_stage === '') &&
+ x.type === t.params.type
+ );
+ t.expectCompileResult(expectation, code);
+ });
+
+g.test('type')
+ .desc(
+ `Test that each @builtin attribute is validated against the required type of that built-in variable.`
+ )
+ .params(u =>
+ u
+ .combineWithParams(kBuiltins)
+ .combine('use_struct', [true, false] as const)
+ .combine('target_type', kTestTypes)
+ .beginSubcases()
+ )
+ .fn(t => {
+ let code = '';
+
+ if (t.params.target_type === 'MyStruct') {
+ // Generate a struct that contains the correct built-in type.
+ code += 'struct MyStruct {\n';
+ code += ` value : ${t.params.type}\n`;
+ code += '};\n\n';
+ }
+
+ code += generateShader({
+ attribute: `@builtin(${t.params.name})`,
+ type: t.params.target_type,
+ stage: t.params.stage,
+ io: t.params.io,
+ use_struct: t.params.use_struct,
+ });
+
+ // Expect to pass iff the built-in table contains an entry that matches.
+ const expectation = kBuiltins.some(
+ x =>
+ x.name === t.params.name &&
+ x.stage === t.params.stage &&
+ x.io === t.params.io &&
+ x.type === t.params.target_type
+ );
+ t.expectCompileResult(expectation, code);
+ });
+
+g.test('nesting')
+ .desc(`Test validation of nested built-in variables`)
+ .params(u =>
+ u
+ .combine('target_stage', ['fragment', ''] as const)
+ .combine('target_io', ['in', 'out'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ // Generate a struct that contains a sample_mask builtin, nested inside another struct.
+ let code = `
+ struct Inner {
+ @builtin(sample_mask) value : u32
+ };
+ struct Outer {
+ inner : Inner
+ };`;
+
+ code += generateShader({
+ attribute: '',
+ type: 'Outer',
+ stage: t.params.target_stage,
+ io: t.params.target_io,
+ use_struct: false,
+ });
+
+ // Expect to pass only if the struct is not used for entry point IO.
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('duplicates')
+ .desc(`Test that duplicated built-in variables are validated.`)
+ .params(u =>
+ u
+ // Place two @builtin(sample_mask) attributes onto the entry point function.
+ // We use `sample_mask` as it is valid as both an input and output for the same entry point.
+ // The function:
+ // - has two non-struct parameters (`p1` and `p2`)
+ // - has two struct parameters each with two members (`s1{a,b}` and `s2{a,b}`)
+ // - returns a struct with two members (`ra` and `rb`)
+ // By default, all of these variables will have unique @location() attributes.
+ .combine('first', ['p1', 's1a', 's2a', 'ra'] as const)
+ .combine('second', ['p2', 's1b', 's2b', 'rb'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const p1 =
+ t.params.first === 'p1' ? '@builtin(sample_mask)' : '@location(1) @interpolate(flat)';
+ const p2 =
+ t.params.second === 'p2' ? '@builtin(sample_mask)' : '@location(2) @interpolate(flat)';
+ const s1a =
+ t.params.first === 's1a' ? '@builtin(sample_mask)' : '@location(3) @interpolate(flat)';
+ const s1b =
+ t.params.second === 's1b' ? '@builtin(sample_mask)' : '@location(4) @interpolate(flat)';
+ const s2a =
+ t.params.first === 's2a' ? '@builtin(sample_mask)' : '@location(5) @interpolate(flat)';
+ const s2b =
+ t.params.second === 's2b' ? '@builtin(sample_mask)' : '@location(6) @interpolate(flat)';
+ const ra =
+ t.params.first === 'ra' ? '@builtin(sample_mask)' : '@location(1) @interpolate(flat)';
+ const rb =
+ t.params.second === 'rb' ? '@builtin(sample_mask)' : '@location(2) @interpolate(flat)';
+ const code = `
+ struct S1 {
+ ${s1a} a : u32,
+ ${s1b} b : u32,
+ };
+ struct S2 {
+ ${s2a} a : u32,
+ ${s2b} b : u32,
+ };
+ struct R {
+ ${ra} a : u32,
+ ${rb} b : u32,
+ };
+ @fragment
+ fn main(${p1} p1 : u32,
+ ${p2} p2 : u32,
+ s1 : S1,
+ s2 : S2,
+ ) -> R {
+ return R();
+ }
+ `;
+
+ // The test should fail if both @builtin(sample_mask) attributes are on the input parameters
+ // or structures, or it they are both on the output struct. Otherwise it should pass.
+ const firstIsRet = t.params.first === 'ra';
+ const secondIsRet = t.params.second === 'rb';
+ const expectation = firstIsRet !== secondIsRet;
+ t.expectCompileResult(expectation, code);
+ });
+
+g.test('missing_vertex_position')
+ .desc(`Test that vertex shaders are required to output @builtin(position).`)
+ .params(u =>
+ u
+ .combine('use_struct', [true, false] as const)
+ .combine('attribute', ['@builtin(position)', '@location(0)'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = `
+ struct S {
+ ${t.params.attribute} value : vec4<f32>
+ };
+
+ @vertex
+ fn main() -> ${t.params.use_struct ? 'S' : `${t.params.attribute} vec4<f32>`} {
+ return ${t.params.use_struct ? 'S' : 'vec4<f32>'}();
+ }
+ `;
+
+ // Expect to pass only when using @builtin(position).
+ t.expectCompileResult(t.params.attribute === '@builtin(position)', code);
+ });
+
+g.test('reuse_builtin_name')
+ .desc(`Test that a builtin name can be used in different contexts`)
+ .params(u =>
+ u
+ .combineWithParams(kBuiltins)
+ .combine('use', ['type_name', 'struct', 'function', 'module-var', 'function-var'])
+ )
+ .fn(t => {
+ let code = '';
+ if (t.params.use === 'type_name') {
+ code += `type ${t.params.name} = i32;`;
+ } else if (t.params.use === `struct`) {
+ code += `struct ${t.params.name} { i: f32, }`;
+ } else if (t.params.use === `function`) {
+ code += `fn ${t.params.name}() {}`;
+ } else if (t.params.use === `module-var`) {
+ code += `const ${t.params.name} = 1;`;
+ } else if (t.params.use === `function-var`) {
+ code += `fn test() { let ${t.params.name} = 1; }`;
+ }
+ t.expectCompileResult(true, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/entry_point.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/entry_point.spec.ts
new file mode 100644
index 0000000000..9aa7319348
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/entry_point.spec.ts
@@ -0,0 +1,141 @@
+export const description = `Validation tests for attributes and entry point requirements`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('missing_attribute_on_param')
+ .desc(`Test that an entry point without an IO attribute on one of its parameters is rejected.`)
+ .params(u =>
+ u.combine('target_stage', ['', 'vertex', 'fragment', 'compute'] as const).beginSubcases()
+ )
+ .fn(t => {
+ const vertex_attr = t.params.target_stage === 'vertex' ? '' : '@location(1)';
+ const fragment_attr = t.params.target_stage === 'fragment' ? '' : '@location(1)';
+ const compute_attr = t.params.target_stage === 'compute' ? '' : '@builtin(workgroup_id)';
+ const code = `
+@vertex
+fn vert_main(@location(0) a : f32,
+ ${vertex_attr} b : f32,
+@ location(2) c : f32) -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+}
+
+@fragment
+fn frag_main(@location(0) a : f32,
+ ${fragment_attr} b : f32,
+@ location(2) c : f32) {
+}
+
+@compute @workgroup_size(1)
+fn comp_main(@builtin(global_invocation_id) a : vec3<u32>,
+ ${compute_attr} b : vec3<u32>,
+ @builtin(local_invocation_id) c : vec3<u32>) {
+}
+`;
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('missing_attribute_on_param_struct')
+ .desc(
+ `Test that an entry point struct parameter without an IO attribute on one of its members is rejected.`
+ )
+ .params(u =>
+ u.combine('target_stage', ['', 'vertex', 'fragment', 'compute'] as const).beginSubcases()
+ )
+ .fn(t => {
+ const vertex_attr = t.params.target_stage === 'vertex' ? '' : '@location(1)';
+ const fragment_attr = t.params.target_stage === 'fragment' ? '' : '@location(1)';
+ const compute_attr = t.params.target_stage === 'compute' ? '' : '@builtin(workgroup_id)';
+ const code = `
+struct VertexInputs {
+ @location(0) a : f32,
+ ${vertex_attr} b : f32,
+@ location(2) c : f32,
+};
+struct FragmentInputs {
+ @location(0) a : f32,
+ ${fragment_attr} b : f32,
+@ location(2) c : f32,
+};
+struct ComputeInputs {
+ @builtin(global_invocation_id) a : vec3<u32>,
+ ${compute_attr} b : vec3<u32>,
+ @builtin(local_invocation_id) c : vec3<u32>,
+};
+
+@vertex
+fn vert_main(inputs : VertexInputs) -> @builtin(position) vec4<f32> {
+ return vec4<f32>();
+}
+
+@fragment
+fn frag_main(inputs : FragmentInputs) {
+}
+
+@compute @workgroup_size(1)
+fn comp_main(inputs : ComputeInputs) {
+}
+`;
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('missing_attribute_on_return_type')
+ .desc(`Test that an entry point without an IO attribute on its return type is rejected.`)
+ .params(u => u.combine('target_stage', ['', 'vertex', 'fragment'] as const).beginSubcases())
+ .fn(t => {
+ const vertex_attr = t.params.target_stage === 'vertex' ? '' : '@builtin(position)';
+ const fragment_attr = t.params.target_stage === 'fragment' ? '' : '@location(0)';
+ const code = `
+@vertex
+fn vert_main() -> ${vertex_attr} vec4<f32> {
+ return vec4<f32>();
+}
+
+@fragment
+fn frag_main() -> ${fragment_attr} vec4<f32> {
+ return vec4<f32>();
+}
+`;
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('missing_attribute_on_return_type_struct')
+ .desc(
+ `Test that an entry point struct return type without an IO attribute on one of its members is rejected.`
+ )
+ .params(u => u.combine('target_stage', ['', 'vertex', 'fragment'] as const).beginSubcases())
+ .fn(t => {
+ const vertex_attr = t.params.target_stage === 'vertex' ? '' : '@location(1)';
+ const fragment_attr = t.params.target_stage === 'fragment' ? '' : '@location(1)';
+ const code = `
+struct VertexOutputs {
+ @location(0) a : f32,
+ ${vertex_attr} b : f32,
+ @builtin(position) c : vec4<f32>,
+};
+struct FragmentOutputs {
+ @location(0) a : f32,
+ ${fragment_attr} b : f32,
+@ location(2) c : f32,
+};
+
+@vertex
+fn vert_main() -> VertexOutputs {
+ return VertexOutputs();
+}
+
+@fragment
+fn frag_main() -> FragmentOutputs {
+ return FragmentOutputs();
+}
+`;
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('no_entry_point_provided')
+ .desc(`Tests that a shader without an entry point is accepted`)
+ .fn(t => {
+ t.expectCompileResult(true, 'fn main() {}');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/interpolate.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/interpolate.spec.ts
new file mode 100644
index 0000000000..eb727d0662
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/interpolate.spec.ts
@@ -0,0 +1,144 @@
+export const description = `Validation tests for the interpolate attribute`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+import { generateShader } from './util.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+// List of valid interpolation attributes.
+const kValidInterpolationAttributes = new Set([
+ '',
+ '@interpolate(flat)',
+ '@interpolate(perspective)',
+ '@interpolate(perspective, center)',
+ '@interpolate(perspective, centroid)',
+ '@interpolate(perspective, sample)',
+ '@interpolate(linear)',
+ '@interpolate(linear, center)',
+ '@interpolate(linear, centroid)',
+ '@interpolate(linear, sample)',
+]);
+
+g.test('type_and_sampling')
+ .desc(`Test that all combinations of interpolation type and sampling are validated correctly.`)
+ .params(u =>
+ u
+ .combine('stage', ['vertex', 'fragment'] as const)
+ .combine('io', ['in', 'out'] as const)
+ .combine('use_struct', [true, false] as const)
+ .combine('type', [
+ '',
+ 'flat',
+ 'perspective',
+ 'linear',
+ 'center', // Invalid as first param
+ 'centroid', // Invalid as first param
+ 'sample', // Invalid as first param
+ ] as const)
+ .combine('sampling', [
+ '',
+ 'center',
+ 'centroid',
+ 'sample',
+ 'flat', // Invalid as second param
+ 'perspective', // Invalid as second param
+ 'linear', // Invalid as second param
+ ] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ if (t.params.stage === 'vertex' && t.params.use_struct === false) {
+ t.skip('vertex output must include a position builtin, so must use a struct');
+ }
+
+ let interpolate = '';
+ if (t.params.type !== '' || t.params.sampling !== '') {
+ interpolate = '@interpolate(';
+ if (t.params.type !== '') {
+ interpolate += `${t.params.type}`;
+ }
+ if (t.params.sampling !== '') {
+ interpolate += `, ${t.params.sampling}`;
+ }
+ interpolate += `)`;
+ }
+ const code = generateShader({
+ attribute: '@location(0)' + interpolate,
+ type: 'f32',
+ stage: t.params.stage,
+ io: t.params.io,
+ use_struct: t.params.use_struct,
+ });
+
+ t.expectCompileResult(kValidInterpolationAttributes.has(interpolate), code);
+ });
+
+g.test('require_location')
+ .desc(`Test that the interpolate attribute is only accepted with user-defined IO.`)
+ .params(u =>
+ u
+ .combine('stage', ['vertex', 'fragment'] as const)
+ .combine('attribute', ['@location(0)', '@builtin(position)'] as const)
+ .combine('use_struct', [true, false] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ if (
+ t.params.stage === 'vertex' &&
+ t.params.use_struct === false &&
+ !t.params.attribute.includes('position')
+ ) {
+ t.skip('vertex output must include a position builtin, so must use a struct');
+ }
+
+ const code = generateShader({
+ attribute: t.params.attribute + `@interpolate(flat)`,
+ type: 'vec4<f32>',
+ stage: t.params.stage,
+ io: t.params.stage === 'fragment' ? 'in' : 'out',
+ use_struct: t.params.use_struct,
+ });
+ t.expectCompileResult(t.params.attribute === '@location(0)', code);
+ });
+
+g.test('integral_types')
+ .desc(`Test that the implementation requires @interpolate(flat) for integral user-defined IO.`)
+ .params(u =>
+ u
+ .combine('stage', ['vertex', 'fragment'] as const)
+ .combine('type', ['i32', 'u32', 'vec2<i32>', 'vec4<u32>'] as const)
+ .combine('use_struct', [true, false] as const)
+ .combine('attribute', kValidInterpolationAttributes)
+ .beginSubcases()
+ )
+ .fn(t => {
+ if (t.params.stage === 'vertex' && t.params.use_struct === false) {
+ t.skip('vertex output must include a position builtin, so must use a struct');
+ }
+
+ const code = generateShader({
+ attribute: '@location(0)' + t.params.attribute,
+ type: t.params.type,
+ stage: t.params.stage,
+ io: t.params.stage === 'vertex' ? 'out' : 'in',
+ use_struct: t.params.use_struct,
+ });
+
+ t.expectCompileResult(t.params.attribute === '@interpolate(flat)', code);
+ });
+
+g.test('duplicate')
+ .desc(`Test that the interpolate attribute can only be applied once.`)
+ .params(u => u.combine('attr', ['', '@interpolate(flat)'] as const))
+ .fn(t => {
+ const code = generateShader({
+ attribute: `@location(0) @interpolate(flat) ${t.params.attr}`,
+ type: 'vec4<f32>',
+ stage: 'fragment',
+ io: 'in',
+ use_struct: false,
+ });
+ t.expectCompileResult(t.params.attr === '', code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/invariant.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/invariant.spec.ts
new file mode 100644
index 0000000000..6ae3480661
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/invariant.spec.ts
@@ -0,0 +1,88 @@
+export const description = `Validation tests for the invariant attribute`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+import { kBuiltins } from './builtins.spec.js';
+import { generateShader } from './util.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('valid_only_with_vertex_position_builtin')
+ .desc(`Test that the invariant attribute is only accepted with the vertex position builtin`)
+ .params(u =>
+ u
+ .combineWithParams(kBuiltins)
+ .combine('use_struct', [true, false] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = generateShader({
+ attribute: `@builtin(${t.params.name}) @invariant`,
+ type: t.params.type,
+ stage: t.params.stage,
+ io: t.params.io,
+ use_struct: t.params.use_struct,
+ });
+
+ t.expectCompileResult(t.params.name === 'position', code);
+ });
+
+g.test('not_valid_on_user_defined_io')
+ .desc(`Test that the invariant attribute is not accepted on user-defined IO attributes.`)
+ .params(u => u.combine('use_invariant', [true, false] as const).beginSubcases())
+ .fn(t => {
+ const invariant = t.params.use_invariant ? '@invariant' : '';
+ const code = `
+ struct VertexOut {
+ @location(0) ${invariant} loc0 : vec4<f32>,
+ @builtin(position) position : vec4<f32>,
+ };
+ @vertex
+ fn main() -> VertexOut {
+ return VertexOut();
+ }
+ `;
+ t.expectCompileResult(!t.params.use_invariant, code);
+ });
+
+g.test('invalid_use_of_parameters')
+ .desc(`Test that no parameters are accepted for the invariant attribute`)
+ .params(u => u.combine('suffix', ['', '()', '(0)'] as const).beginSubcases())
+ .fn(t => {
+ const code = `
+ struct VertexOut {
+ @builtin(position) @invariant${t.params.suffix} position : vec4<f32>
+ };
+ @vertex
+ fn main() -> VertexOut {
+ return VertexOut();
+ }
+ `;
+ t.expectCompileResult(t.params.suffix === '', code);
+ });
+
+g.test('duplicate')
+ .desc(`Test that the invariant attribute can only be applied once.`)
+ .params(u =>
+ u
+ .combineWithParams(kBuiltins)
+ .combine('use_struct', [true, false] as const)
+ .combine('attr', ['', '@invariant'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ if (t.params.name !== 'position') {
+ t.skip('only valid with position');
+ }
+
+ const code = generateShader({
+ attribute: `@builtin(${t.params.name}) @invariant ${t.params.attr}`,
+ type: t.params.type,
+ stage: t.params.stage,
+ io: t.params.io,
+ use_struct: t.params.use_struct,
+ });
+
+ t.expectCompileResult(t.params.attr === '', code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts
new file mode 100644
index 0000000000..64fcc4c284
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/locations.spec.ts
@@ -0,0 +1,259 @@
+export const description = `Validation tests for entry point user-defined IO`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+import { generateShader } from './util.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kValidLocationTypes = new Set([
+ 'f16',
+ 'f32',
+ 'i32',
+ 'u32',
+ 'vec2<f32>',
+ 'vec2<i32>',
+ 'vec2<u32>',
+ 'vec3<f32>',
+ 'vec3<i32>',
+ 'vec3<u32>',
+ 'vec4<f32>',
+ 'vec4<i32>',
+ 'vec4<u32>',
+ 'vec2h',
+ 'vec2f',
+ 'vec2i',
+ 'vec2u',
+ 'vec3h',
+ 'vec3f',
+ 'vec3i',
+ 'vec3u',
+ 'vec4h',
+ 'vec4f',
+ 'vec4i',
+ 'vec4u',
+ 'MyAlias',
+]);
+
+const kInvalidLocationTypes = new Set([
+ 'bool',
+ 'vec2<bool>',
+ 'vec3<bool>',
+ 'vec4<bool>',
+ 'mat2x2<f32>',
+ 'mat2x3<f32>',
+ 'mat2x4<f32>',
+ 'mat3x2<f32>',
+ 'mat3x3<f32>',
+ 'mat3x4<f32>',
+ 'mat4x2<f32>',
+ 'mat4x3<f32>',
+ 'mat4x4<f32>',
+ 'mat2x2f',
+ 'mat2x3f',
+ 'mat2x4f',
+ 'mat3x2f',
+ 'mat3x3f',
+ 'mat3x4f',
+ 'mat4x2f',
+ 'mat4x3f',
+ 'mat4x4f',
+ 'mat2x2h',
+ 'mat2x3h',
+ 'mat2x4h',
+ 'mat3x2h',
+ 'mat3x3h',
+ 'mat3x4h',
+ 'mat4x2h',
+ 'mat4x3h',
+ 'mat4x4h',
+ 'array<f32, 12>',
+ 'array<i32, 12>',
+ 'array<u32, 12>',
+ 'array<bool, 12>',
+ 'atomic<i32>',
+ 'atomic<u32>',
+ 'MyStruct',
+ 'texture_1d<i32>',
+ 'texture_2d<f32>',
+ 'texture_2d_array<i32>',
+ 'texture_3d<f32>',
+ 'texture_cube<u32>',
+ 'texture_cube_array<i32>',
+ 'texture_multisampled_2d<i32>',
+ 'texture_external',
+ 'texture_storage_1d<rgba8unorm, write>',
+ 'texture_storage_2d<rg32float, write>',
+ 'texture_storage_2d_array<r32float, write>',
+ 'texture_storage_3d<r32float, write>',
+ 'texture_depth_2d',
+ 'texture_depth_2d_array',
+ 'texture_depth_cube',
+ 'texture_depth_cube_array',
+ 'texture_depth_multisampled_2d',
+ 'sampler',
+ 'sampler_comparison',
+]);
+
+g.test('stage_inout')
+ .desc(`Test validation of user-defined IO stage and in/out usage`)
+ .params(u =>
+ u
+ .combine('use_struct', [true, false] as const)
+ .combine('target_stage', ['vertex', 'fragment', 'compute'] as const)
+ .combine('target_io', ['in', 'out'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const code = generateShader({
+ attribute: '@location(0)',
+ type: 'f32',
+ stage: t.params.target_stage,
+ io: t.params.target_io,
+ use_struct: t.params.use_struct,
+ });
+
+ // Expect to fail for compute shaders or when used as a non-struct vertex output (since the
+ // position built-in must also be specified).
+ const expectation =
+ t.params.target_stage === 'fragment' ||
+ (t.params.target_stage === 'vertex' && (t.params.target_io === 'in' || t.params.use_struct));
+ t.expectCompileResult(expectation, code);
+ });
+
+g.test('type')
+ .desc(`Test validation of user-defined IO types`)
+ .params(u =>
+ u
+ .combine('use_struct', [true, false] as const)
+ .combine('type', new Set([...kValidLocationTypes, ...kInvalidLocationTypes]))
+ .beginSubcases()
+ )
+ .beforeAllSubcases(t => {
+ if (
+ t.params.type === 'f16' ||
+ ((t.params.type.startsWith('mat') || t.params.type.startsWith('vec')) &&
+ t.params.type.endsWith('h'))
+ ) {
+ t.selectDeviceOrSkipTestCase('shader-f16');
+ }
+ })
+ .fn(t => {
+ let code = '';
+
+ if (
+ t.params.type === 'f16' ||
+ ((t.params.type.startsWith('mat') || t.params.type.startsWith('vec')) &&
+ t.params.type.endsWith('h'))
+ ) {
+ code += 'enable f16;\n';
+ }
+
+ if (t.params.type === 'MyStruct') {
+ // Generate a struct that contains a valid type.
+ code += `struct MyStruct {
+ value : f32,
+ }
+ `;
+ }
+ if (t.params.type === 'MyAlias') {
+ code += 'type MyAlias = i32;\n';
+ }
+
+ code += generateShader({
+ attribute: '@location(0) @interpolate(flat)',
+ type: t.params.type,
+ stage: 'fragment',
+ io: 'in',
+ use_struct: t.params.use_struct,
+ });
+
+ t.expectCompileResult(kValidLocationTypes.has(t.params.type), code);
+ });
+
+g.test('nesting')
+ .desc(`Test validation of nested user-defined IO`)
+ .params(u =>
+ u
+ .combine('target_stage', ['vertex', 'fragment', ''] as const)
+ .combine('target_io', ['in', 'out'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ let code = '';
+
+ // Generate a struct that contains a valid type.
+ code += `struct Inner {
+ @location(0) value : f32,
+ }
+ struct Outer {
+ inner : Inner,
+ }
+ `;
+
+ code += generateShader({
+ attribute: '',
+ type: 'Outer',
+ stage: t.params.target_stage,
+ io: t.params.target_io,
+ use_struct: false,
+ });
+
+ // Expect to pass only if the struct is not used for entry point IO.
+ t.expectCompileResult(t.params.target_stage === '', code);
+ });
+
+g.test('duplicates')
+ .desc(`Test that duplicated user-defined IO attributes are validated.`)
+ .params(u =>
+ u
+ // Place two @location(0) attributes onto the entry point function.
+ // The function:
+ // - has two non-struct parameters (`p1` and `p2`)
+ // - has two struct parameters each with two members (`s1{a,b}` and `s2{a,b}`)
+ // - returns a struct with two members (`ra` and `rb`)
+ // By default, all of these user-defined IO variables will have unique location attributes.
+ .combine('first', ['p1', 's1a', 's2a', 'ra'] as const)
+ .combine('second', ['p2', 's1b', 's2b', 'rb'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const p1 = t.params.first === 'p1' ? '0' : '1';
+ const p2 = t.params.second === 'p2' ? '0' : '2';
+ const s1a = t.params.first === 's1a' ? '0' : '3';
+ const s1b = t.params.second === 's1b' ? '0' : '4';
+ const s2a = t.params.first === 's2a' ? '0' : '5';
+ const s2b = t.params.second === 's2b' ? '0' : '6';
+ const ra = t.params.first === 'ra' ? '0' : '1';
+ const rb = t.params.second === 'rb' ? '0' : '2';
+ const code = `
+ struct S1 {
+ @location(${s1a}) a : f32,
+ @location(${s1b}) b : f32,
+ };
+ struct S2 {
+ @location(${s2a}) a : f32,
+ @location(${s2b}) b : f32,
+ };
+ struct R {
+ @location(${ra}) a : f32,
+ @location(${rb}) b : f32,
+ };
+ @fragment
+ fn main(@location(${p1}) p1 : f32,
+ @location(${p2}) p2 : f32,
+ s1 : S1,
+ s2 : S2,
+ ) -> R {
+ return R();
+ }
+ `;
+
+ // The test should fail if both @location(0) attributes are on the input parameters or
+ // structures, or it they are both on the output struct. Otherwise it should pass.
+ const firstIsRet = t.params.first === 'ra';
+ const secondIsRet = t.params.second === 'rb';
+ const expectation = firstIsRet !== secondIsRet;
+ t.expectCompileResult(expectation, code);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/util.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/util.ts
new file mode 100644
index 0000000000..f70aaee957
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_io/util.ts
@@ -0,0 +1,79 @@
+/**
+ * Generate an entry point that uses an entry point IO variable.
+ *
+ * @param {Object} params
+ * @param params.attribute The entry point IO attribute.
+ * @param params.type The type to use for the entry point IO variable.
+ * @param params.stage The shader stage.
+ * @param params.io An "in|out" string specifying whether the entry point IO is an input or an output.
+ * @param params.use_struct True to wrap the entry point IO in a struct.
+ * @returns The generated shader code.
+ */
+export function generateShader({
+ attribute,
+ type,
+ stage,
+ io,
+ use_struct,
+}: {
+ attribute: string;
+ type: string;
+ stage: string;
+ io: string;
+ use_struct: boolean;
+}) {
+ let code = '';
+
+ if (use_struct) {
+ // Generate a struct that wraps the entry point IO variable.
+ code += 'struct S {\n';
+ code += ` ${attribute} value : ${type},\n`;
+ if (stage === 'vertex' && io === 'out' && !attribute.includes('builtin(position)')) {
+ // Add position builtin for vertex outputs.
+ code += ` @builtin(position) position : vec4<f32>,\n`;
+ }
+ code += '};\n\n';
+ }
+
+ if (stage !== '') {
+ // Generate the entry point attributes.
+ code += `@${stage}`;
+ if (stage === 'compute') {
+ code += ' @workgroup_size(1)';
+ }
+ }
+
+ // Generate the entry point parameter and return type.
+ let param = '';
+ let retType = '';
+ let retVal = '';
+ if (io === 'in') {
+ if (use_struct) {
+ param = `in : S`;
+ } else {
+ param = `${attribute} value : ${type}`;
+ }
+
+ // Vertex shaders must always return `@builtin(position)`.
+ if (stage === 'vertex') {
+ retType = `-> @builtin(position) vec4<f32>`;
+ retVal = `return vec4<f32>();`;
+ }
+ } else if (io === 'out') {
+ if (use_struct) {
+ retType = '-> S';
+ retVal = `return S();`;
+ } else {
+ retType = `-> ${attribute} ${type}`;
+ retVal = `return ${type}();`;
+ }
+ }
+
+ code += `
+ fn main(${param}) ${retType} {
+ ${retVal}
+ }
+ `;
+
+ return code;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_validation_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_validation_test.ts
new file mode 100644
index 0000000000..40d7867a66
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/shader_validation_test.ts
@@ -0,0 +1,76 @@
+import { ErrorWithExtra } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+
+/**
+ * Base fixture for WGSL shader validation tests.
+ */
+export class ShaderValidationTest extends GPUTest {
+ /**
+ * Add a test expectation for whether a createShaderModule call succeeds or not.
+ *
+ * @example
+ * ```ts
+ * t.expectCompileResult(true, `wgsl code`); // Expect success
+ * t.expectCompileResult(false, `wgsl code`); // Expect validation error with any error string
+ * ```
+ */
+ expectCompileResult(expectedResult: boolean, code: string) {
+ let shaderModule: GPUShaderModule;
+ this.expectGPUError(
+ 'validation',
+ () => {
+ shaderModule = this.device.createShaderModule({ code });
+ },
+ expectedResult !== true
+ );
+
+ const error = new ErrorWithExtra('', () => ({ shaderModule }));
+ this.eventualAsyncExpectation(async () => {
+ const compilationInfo = await shaderModule!.compilationInfo();
+
+ // MAINTENANCE_TODO: Pretty-print error messages with source context.
+ const messagesLog = compilationInfo.messages
+ .map(m => `${m.lineNum}:${m.linePos}: ${m.type}: ${m.message}`)
+ .join('\n');
+ error.extra.compilationInfo = compilationInfo;
+
+ if (compilationInfo.messages.some(m => m.type === 'error')) {
+ if (expectedResult) {
+ error.message = `Unexpected compilationInfo 'error' message.\n` + messagesLog;
+ this.rec.validationFailed(error);
+ } else {
+ error.message = `Found expected compilationInfo 'error' message.\n` + messagesLog;
+ this.rec.debug(error);
+ }
+ } else {
+ if (!expectedResult) {
+ error.message = `Missing expected compilationInfo 'error' message.\n` + messagesLog;
+ this.rec.validationFailed(error);
+ } else {
+ error.message = `No compilationInfo 'error' messages, as expected.\n` + messagesLog;
+ this.rec.debug(error);
+ }
+ }
+ });
+ }
+
+ /**
+ * Wraps the code fragment into an entry point.
+ *
+ * @example
+ * ```ts
+ * t.wrapInEntryPoint(`var i = 0;`);
+ * ```
+ */
+ wrapInEntryPoint(code: string, enabledExtensions: string[] = []) {
+ const enableDirectives = enabledExtensions.map(x => `enable ${x};`).join('\n ');
+
+ return `
+ ${enableDirectives}
+
+ @compute @workgroup_size(1)
+ fn main() {
+ ${code}
+ }`;
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/static_assert/static_assert.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/static_assert/static_assert.spec.ts
new file mode 100644
index 0000000000..24db6bd5af
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/validation/static_assert/static_assert.spec.ts
@@ -0,0 +1,70 @@
+export const description = `Validation tests for static_assert`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+/**
+ * Builds a static_assert() statement, which checks that @p expr is equal to @p expect_true.
+ * @param expect_true true if @p expr should evaluate to true
+ * @param expr the constant expression
+ * @param scope module-scope or function-scope constant expression
+ * @returns the WGSL code
+ */
+function buildStaticAssert(expect_true: boolean, expr: string, scope: 'module' | 'function') {
+ const stmt = expect_true ? `static_assert ${expr};` : `static_assert !(${expr});`;
+ return scope === 'module' ? stmt : `fn f() { ${stmt} }`;
+}
+
+const kConditionCases = {
+ true_literal: `true`,
+ not_false: `!false`,
+ const_eq_literal_int: `one == 1`,
+ const_eq_literal_float: `one == 1.0`,
+ binary_op_eq_const: `one+1 == two`,
+ any: `any(vec3(false, true, false))`,
+ min_max: `min(three, max(two, one)) == 2`,
+};
+
+g.test('constant_expression')
+ .desc(`Test that static_assert validates the condition expression.`)
+ .params(u =>
+ u
+ .combine('case', Object.keys(kConditionCases) as Array<keyof typeof kConditionCases>)
+ .combine('scope', ['module', 'function'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const constants = `
+const one = 1;
+const two = 2;
+const three = 2;
+`;
+ const expr = kConditionCases[t.params.case];
+ t.expectCompileResult(true, constants + buildStaticAssert(true, expr, t.params.scope));
+ t.expectCompileResult(false, constants + buildStaticAssert(false, expr, t.params.scope));
+ });
+
+g.test('evaluation_stage')
+ .desc(`Test that the static_assert expression must be a constant expression.`)
+ .params(u =>
+ u
+ .combine('scope', ['module', 'function'] as const)
+ .combine('stage', ['constant', 'override', 'runtime'] as const)
+ .beginSubcases()
+ )
+ .fn(t => {
+ const staticAssert = buildStaticAssert(true, 'value', t.params.scope);
+ switch (t.params.stage) {
+ case 'constant':
+ t.expectCompileResult(true, `const value = true;\n${staticAssert}`);
+ break;
+ case 'override':
+ t.expectCompileResult(false, `override value = true;\n${staticAssert}`);
+ break;
+ case 'runtime':
+ t.expectCompileResult(false, `var<private> value = true;\n${staticAssert}`);
+ break;
+ }
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/shader/values.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/values.ts
new file mode 100644
index 0000000000..38a2fe46f0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/shader/values.ts
@@ -0,0 +1,91 @@
+export const description = `Special and sample values for WGSL scalar types`;
+
+import { assert } from '../../common/util/util.js';
+import { uint32ToFloat32 } from '../util/conversion.js';
+
+/** Returns an array of subnormal f32 numbers.
+ * Subnormals are non-zero finite numbers with the minimum representable
+ * exponent.
+ */
+export function subnormalF32Examples(): Array<number> {
+ // The results, as uint32 values.
+ const result_as_bits: number[] = [];
+
+ const max_mantissa = 0x7f_ffff;
+ const sign_bits = [0, 0x8000_0000];
+ for (const sign_bit of sign_bits) {
+ // exponent bits must be zero.
+ const sign_and_exponent = sign_bit;
+
+ // Set all bits
+ result_as_bits.push(sign_and_exponent | max_mantissa);
+
+ // Set each of the lower bits individually.
+ for (let lower_bits = 1; lower_bits <= max_mantissa; lower_bits <<= 1) {
+ result_as_bits.push(sign_and_exponent | lower_bits);
+ }
+ }
+ assert(
+ result_as_bits.length === 2 * (1 + 23),
+ 'subnormal number sample count is ' + result_as_bits.length.toString()
+ );
+ return result_as_bits.map(u => uint32ToFloat32(u));
+}
+
+/** Returns an array of normal f32 numbers.
+ * Normal numbers are not: zero, Nan, infinity, subnormal.
+ */
+export function normalF32Examples(): Array<number> {
+ const result: number[] = [1.0, -2.0];
+
+ const max_mantissa_as_bits = 0x7f_ffff;
+ const min_exponent_as_bits = 0x0080_0000;
+ const max_exponent_as_bits = 0x7f00_0000; // Max normal exponent
+ const sign_bits = [0, 0x8000_0000];
+ for (const sign_bit of sign_bits) {
+ for (let e = min_exponent_as_bits; e <= max_exponent_as_bits; e += min_exponent_as_bits) {
+ const sign_and_exponent = sign_bit | e;
+
+ // Set zero mantissa bits
+ result.push(uint32ToFloat32(sign_and_exponent));
+ // Set all mantissa bits
+ result.push(uint32ToFloat32(sign_and_exponent | max_mantissa_as_bits));
+
+ // Set each of the lower bits individually.
+ for (let lower_bits = 1; lower_bits <= max_mantissa_as_bits; lower_bits <<= 1) {
+ result.push(uint32ToFloat32(sign_and_exponent | lower_bits));
+ }
+ }
+ }
+ assert(
+ result.length === 2 + 2 * 254 * 25,
+ 'normal number sample count is ' + result.length.toString()
+ );
+ return result;
+}
+
+/** Returns an array of 32-bit NaNs, as Uint32 bit patterns.
+ * NaNs have: maximum exponent, but the mantissa is not zero.
+ */
+export function nanF32BitsExamples(): Array<number> {
+ const result: number[] = [];
+ const exponent_bit = 0x7f80_0000;
+ const sign_bits = [0, 0x8000_0000];
+ for (const sign_bit of sign_bits) {
+ const sign_and_exponent = sign_bit | exponent_bit;
+ const bits = sign_and_exponent | 0x40_0000;
+ // Only the most significant bit of the mantissa is set.
+ result.push(bits);
+
+ // Quiet and signalling NaNs differ based on the most significant bit
+ // of the mantissa. Try both.
+ for (const quiet_signalling of [0, 0x40_0000]) {
+ // Set each of the lower bits.
+ for (let lower_bits = 1; lower_bits < 0x40_0000; lower_bits <<= 1) {
+ const bits = sign_and_exponent | quiet_signalling | lower_bits;
+ result.push(bits);
+ }
+ }
+ }
+ return result;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/buffer.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/buffer.ts
new file mode 100644
index 0000000000..a7d154a7e6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/buffer.ts
@@ -0,0 +1,23 @@
+import { memcpy, TypedArrayBufferView } from '../../common/util/util.js';
+
+import { align } from './math.js';
+
+/**
+ * Creates a buffer with the contents of some TypedArray.
+ * The buffer size will always be aligned to 4 as we set mappedAtCreation === true when creating the
+ * buffer.
+ */
+export function makeBufferWithContents(
+ device: GPUDevice,
+ dataArray: TypedArrayBufferView,
+ usage: GPUBufferUsageFlags
+): GPUBuffer {
+ const buffer = device.createBuffer({
+ mappedAtCreation: true,
+ size: align(dataArray.byteLength, 4),
+ usage,
+ });
+ memcpy({ src: dataArray }, { dst: buffer.getMappedRange() });
+ buffer.unmap();
+ return buffer;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts
new file mode 100644
index 0000000000..325cae41a2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/check_contents.ts
@@ -0,0 +1,245 @@
+// MAINTENANCE_TODO: The "checkThingTrue" naming is confusing; these must be used with `expectOK`
+// or the result is dropped on the floor. Rename these to things like `typedArrayIsOK`(??) to
+// make it clearer.
+// MAINTENANCE_TODO: Also, audit to make sure we aren't dropping any on the floor. Consider a
+// no-ignored-return lint check if we can find one that we can use.
+
+import {
+ assert,
+ ErrorWithExtra,
+ iterRange,
+ range,
+ TypedArrayBufferView,
+ TypedArrayBufferViewConstructor,
+} from '../../common/util/util.js';
+
+import { float16BitsToFloat32 } from './conversion.js';
+import { generatePrettyTable } from './pretty_diff_tables.js';
+
+/** Generate an expected value at `index`, to test for equality with the actual value. */
+export type CheckElementsGenerator = (index: number) => number;
+/** Check whether the actual `value` at `index` is as expected. */
+export type CheckElementsPredicate = (index: number, value: number) => boolean;
+/**
+ * Provides a pretty-printing implementation for a particular CheckElementsPredicate.
+ * This is an array; each element provides info to print an additional row in the error message.
+ */
+export type CheckElementsSupplementalTableRows = Array<{
+ /** Row header. */
+ leftHeader: string;
+ /**
+ * Get the value for a cell in the table with element index `index`.
+ * May be a string or a number; a number will be formatted according to the TypedArray type used.
+ */
+ getValueForCell: (index: number) => number | string;
+}>;
+
+/**
+ * Check whether two `TypedArray`s have equal contents.
+ * Returns `undefined` if the check passes, or an `Error` if not.
+ */
+export function checkElementsEqual(
+ actual: TypedArrayBufferView,
+ expected: TypedArrayBufferView
+): ErrorWithExtra | undefined {
+ assert(actual.constructor === expected.constructor, 'TypedArray type mismatch');
+ assert(actual.length === expected.length, 'size mismatch');
+ return checkElementsEqualGenerated(actual, i => expected[i]);
+}
+
+/**
+ * Check whether each value in a `TypedArray` is between the two corresponding "expected" values
+ * (either `a(i) <= actual[i] <= b(i)` or `a(i) >= actual[i] => b(i)`).
+ */
+export function checkElementsBetween(
+ actual: TypedArrayBufferView,
+ expected: readonly [CheckElementsGenerator, CheckElementsGenerator]
+): ErrorWithExtra | undefined {
+ const error = checkElementsPassPredicate(
+ actual,
+ (index, value) =>
+ value >= Math.min(expected[0](index), expected[1](index)) &&
+ value <= Math.max(expected[0](index), expected[1](index)),
+ {
+ predicatePrinter: [
+ { leftHeader: 'between', getValueForCell: index => expected[0](index) },
+ { leftHeader: 'and', getValueForCell: index => expected[1](index) },
+ ],
+ }
+ );
+ // If there was an error, extend it with additional extras.
+ return error ? new ErrorWithExtra(error, () => ({ expected })) : undefined;
+}
+
+/**
+ * Equivalent to {@link checkElementsBetween} but interpret values as float16 and convert to JS number before comparison.
+ */
+export function checkElementsFloat16Between(
+ actual: TypedArrayBufferView,
+ expected: readonly [TypedArrayBufferView, TypedArrayBufferView]
+): ErrorWithExtra | undefined {
+ assert(actual.BYTES_PER_ELEMENT === 2, 'bytes per element need to be 2 (16bit)');
+ const actualF32 = new Float32Array(actual.length);
+ actual.forEach((v: number, i: number) => {
+ actualF32[i] = float16BitsToFloat32(v);
+ });
+ const expectedF32 = [new Float32Array(expected[0].length), new Float32Array(expected[1].length)];
+ expected[0].forEach((v: number, i: number) => {
+ expectedF32[0][i] = float16BitsToFloat32(v);
+ });
+ expected[1].forEach((v: number, i: number) => {
+ expectedF32[1][i] = float16BitsToFloat32(v);
+ });
+
+ const error = checkElementsPassPredicate(
+ actualF32,
+ (index, value) =>
+ value >= Math.min(expectedF32[0][index], expectedF32[1][index]) &&
+ value <= Math.max(expectedF32[0][index], expectedF32[1][index]),
+ {
+ predicatePrinter: [
+ { leftHeader: 'between', getValueForCell: index => expectedF32[0][index] },
+ { leftHeader: 'and', getValueForCell: index => expectedF32[1][index] },
+ ],
+ }
+ );
+ // If there was an error, extend it with additional extras.
+ return error ? new ErrorWithExtra(error, () => ({ expectedF32 })) : undefined;
+}
+
+/**
+ * Check whether each value in a `TypedArray` is equal to one of the two corresponding "expected"
+ * values (either `actual[i] === a[i]` or `actual[i] === b[i]`)
+ */
+export function checkElementsEqualEither(
+ actual: TypedArrayBufferView,
+ expected: readonly [TypedArrayBufferView, TypedArrayBufferView]
+): ErrorWithExtra | undefined {
+ const error = checkElementsPassPredicate(
+ actual,
+ (index, value) => value === expected[0][index] || value === expected[1][index],
+ {
+ predicatePrinter: [
+ { leftHeader: 'either', getValueForCell: index => expected[0][index] },
+ { leftHeader: 'or', getValueForCell: index => expected[1][index] },
+ ],
+ }
+ );
+ // If there was an error, extend it with additional extras.
+ return error ? new ErrorWithExtra(error, () => ({ expected })) : undefined;
+}
+
+/**
+ * Check whether a `TypedArray`'s contents equal the values produced by a generator function.
+ * Returns `undefined` if the check passes, or an `Error` if not.
+ *
+ * ```text
+ * Array had unexpected contents at indices 2 through 19.
+ * Starting at index 1:
+ * actual == 0x: 00 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00
+ * failed -> xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx
+ * expected == 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * ```
+ *
+ * ```text
+ * Array had unexpected contents at indices 2 through 29.
+ * Starting at index 1:
+ * actual == 0.000 -2.000e+100 -1.000e+100 0.000 1.000e+100 2.000e+100 3.000e+100 4.000e+100 5.000e+100 6.000e+100 7.000e+100 ...
+ * failed -> xx xx xx xx xx xx xx xx xx ...
+ * expected == 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ...
+ * ```
+ */
+export function checkElementsEqualGenerated(
+ actual: TypedArrayBufferView,
+ generator: CheckElementsGenerator
+): ErrorWithExtra | undefined {
+ const error = checkElementsPassPredicate(actual, (index, value) => value === generator(index), {
+ predicatePrinter: [{ leftHeader: 'expected ==', getValueForCell: index => generator(index) }],
+ });
+ // If there was an error, extend it with additional extras.
+ return error ? new ErrorWithExtra(error, () => ({ generator })) : undefined;
+}
+
+/**
+ * Check whether a `TypedArray`'s values pass the provided predicate function.
+ * Returns `undefined` if the check passes, or an `Error` if not.
+ */
+export function checkElementsPassPredicate(
+ actual: TypedArrayBufferView,
+ predicate: CheckElementsPredicate,
+ { predicatePrinter }: { predicatePrinter?: CheckElementsSupplementalTableRows }
+): ErrorWithExtra | undefined {
+ const size = actual.length;
+ const ctor = actual.constructor as TypedArrayBufferViewConstructor;
+ const printAsFloat = ctor === Float32Array || ctor === Float64Array;
+
+ let failedElementsFirstMaybe: number | undefined = undefined;
+ /** Sparse array with `true` for elements that failed. */
+ const failedElements: (true | undefined)[] = [];
+ for (let i = 0; i < size; ++i) {
+ if (!predicate(i, actual[i])) {
+ failedElementsFirstMaybe ??= i;
+ failedElements[i] = true;
+ }
+ }
+
+ if (failedElementsFirstMaybe === undefined) {
+ return undefined;
+ }
+ const failedElementsFirst = failedElementsFirstMaybe;
+ const failedElementsLast = failedElements.length - 1;
+
+ // Include one extra non-failed element at the beginning and end (if they exist), for context.
+ const printElementsStart = Math.max(0, failedElementsFirst - 1);
+ const printElementsEnd = Math.min(size, failedElementsLast + 2);
+ const printElementsCount = printElementsEnd - printElementsStart;
+
+ const numberToString = printAsFloat
+ ? (n: number) => n.toPrecision(4)
+ : (n: number) => intToPaddedHex(n, { byteLength: ctor.BYTES_PER_ELEMENT });
+ const numberPrefix = printAsFloat ? '' : '0x:';
+
+ const printActual = actual.subarray(printElementsStart, printElementsEnd);
+ const printExpected: Array<Iterable<string | number>> = [];
+ if (predicatePrinter) {
+ for (const { leftHeader, getValueForCell: cell } of predicatePrinter) {
+ printExpected.push(
+ (function* () {
+ yield* [leftHeader, ''];
+ yield* iterRange(printElementsCount, i => cell(printElementsStart + i));
+ })()
+ );
+ }
+ }
+
+ const printFailedValueMarkers = (function* () {
+ yield* ['failed ->', ''];
+ yield* range(printElementsCount, i => (failedElements[printElementsStart + i] ? 'xx' : ''));
+ })();
+
+ const opts = {
+ fillToWidth: 120,
+ numberToString,
+ };
+ const msg = `Array had unexpected contents at indices ${failedElementsFirst} through ${failedElementsLast}.
+ Starting at index ${printElementsStart}:
+${generatePrettyTable(opts, [
+ ['actual ==', numberPrefix, ...printActual],
+ printFailedValueMarkers,
+ ...printExpected,
+])}`;
+ return new ErrorWithExtra(msg, () => ({
+ actual: actual.slice(),
+ }));
+}
+
+// Helper helpers
+
+/** Convert an integral `number` into a hex string, padded to the specified `byteLength`. */
+function intToPaddedHex(number: number, { byteLength }: { byteLength: number }) {
+ assert(Number.isInteger(number), 'number must be integer');
+ let s = Math.abs(number).toString(16);
+ if (byteLength) s = s.padStart(byteLength * 2, '0');
+ if (number < 0) s = '-' + s;
+ return s;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts
new file mode 100644
index 0000000000..6d84be8eee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts
@@ -0,0 +1,261 @@
+import { assert, unreachable } from '../../common/util/util.js';
+
+import { multiplyMatrices } from './math.js';
+
+// These color space conversion function definitions are copied directly from
+// CSS Color Module Level 4 Sample Code: https://drafts.csswg.org/css-color/#color-conversion-code
+// *EXCEPT* the conversion matrices are replaced with exact rational forms computed here:
+// https://github.com/kainino0x/exact_css_xyz_matrices
+// using this Rust crate: https://crates.io/crates/rgb_derivation
+// as described for sRGB on this page: https://mina86.com/2019/srgb-xyz-matrix/
+// but using the numbers from the CSS spec: https://www.w3.org/TR/css-color-4/#predefined
+
+// Sample code for color conversions
+// Conversion can also be done using ICC profiles and a Color Management System
+// For clarity, a library is used for matrix multiplication (multiply-matrices.js)
+
+// sRGB-related functions
+
+/**
+ * convert an array of sRGB values
+ * where in-gamut values are in the range [0 - 1]
+ * to linear light (un-companded) form.
+ * https://en.wikipedia.org/wiki/SRGB
+ * Extended transfer function:
+ * for negative values, linear portion is extended on reflection of axis,
+ * then reflected power function is used.
+ */
+function lin_sRGB(RGB: Array<number>) {
+ return RGB.map(val => {
+ const sign = val < 0 ? -1 : 1;
+ const abs = Math.abs(val);
+
+ if (abs < 0.04045) {
+ return val / 12.92;
+ }
+
+ return sign * Math.pow((abs + 0.055) / 1.055, 2.4);
+ });
+}
+
+/**
+ * convert an array of linear-light sRGB values in the range 0.0-1.0
+ * to gamma corrected form
+ * https://en.wikipedia.org/wiki/SRGB
+ * Extended transfer function:
+ * For negative values, linear portion extends on reflection
+ * of axis, then uses reflected pow below that
+ */
+function gam_sRGB(RGB: Array<number>) {
+ return RGB.map(val => {
+ const sign = val < 0 ? -1 : 1;
+ const abs = Math.abs(val);
+
+ if (abs > 0.0031308) {
+ return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
+ }
+
+ return 12.92 * val;
+ });
+}
+
+/**
+ * convert an array of linear-light sRGB values to CIE XYZ
+ * using sRGB's own white, D65 (no chromatic adaptation)
+ */
+function lin_sRGB_to_XYZ(rgb: Array<Array<number>>) {
+ const M = /* prettier-ignore */ [
+ [506752 / 1228815, 87881 / 245763, 12673 / 70218],
+ [ 87098 / 409605, 175762 / 245763, 12673 / 175545],
+ [ 7918 / 409605, 87881 / 737289, 1001167 / 1053270],
+ ];
+ return multiplyMatrices(M, rgb);
+}
+
+/**
+ * convert XYZ to linear-light sRGB
+ * using sRGB's own white, D65 (no chromatic adaptation)
+ */
+function XYZ_to_lin_sRGB(XYZ: Array<Array<number>>) {
+ const M = /* prettier-ignore */ [
+ [ 12831 / 3959, -329 / 214, -1974 / 3959],
+ [-851781 / 878810, 1648619 / 878810, 36519 / 878810],
+ [ 705 / 12673, -2585 / 12673, 705 / 667],
+ ];
+
+ return multiplyMatrices(M, XYZ);
+}
+
+// display-p3-related functions
+
+/**
+ * convert an array of display-p3 RGB values in the range 0.0 - 1.0
+ * to linear light (un-companded) form.
+ */
+function lin_P3(RGB: Array<number>) {
+ return lin_sRGB(RGB); // same as sRGB
+}
+
+/**
+ * convert an array of linear-light display-p3 RGB in the range 0.0-1.0
+ * to gamma corrected form
+ */
+function gam_P3(RGB: Array<number>) {
+ return gam_sRGB(RGB); // same as sRGB
+}
+
+/**
+ * convert an array of linear-light display-p3 values to CIE XYZ
+ * using display-p3's D65 (no chromatic adaptation)
+ */
+function lin_P3_to_XYZ(rgb: Array<Array<number>>) {
+ const M = /* prettier-ignore */ [
+ [608311 / 1250200, 189793 / 714400, 198249 / 1000160],
+ [ 35783 / 156275, 247089 / 357200, 198249 / 2500400],
+ [ 0 / 1, 32229 / 714400, 5220557 / 5000800],
+ ];
+
+ return multiplyMatrices(M, rgb);
+}
+
+/**
+ * convert XYZ to linear-light P3
+ * using display-p3's own white, D65 (no chromatic adaptation)
+ */
+function XYZ_to_lin_P3(XYZ: Array<Array<number>>) {
+ const M = /* prettier-ignore */ [
+ [446124 / 178915, -333277 / 357830, -72051 / 178915],
+ [-14852 / 17905, 63121 / 35810, 423 / 17905],
+ [ 11844 / 330415, -50337 / 660830, 316169 / 330415],
+ ];
+
+ return multiplyMatrices(M, XYZ);
+}
+
+/**
+ * @returns the converted pixels in `{R: number, G: number, B: number, A: number}`.
+ *
+ * Follow conversion steps in CSS Color Module Level 4
+ * https://drafts.csswg.org/css-color/#predefined-to-predefined
+ * display-p3 and sRGB share the same white points.
+ */
+export function displayP3ToSrgb(pixel: {
+ R: number;
+ G: number;
+ B: number;
+ A: number;
+}): { R: number; G: number; B: number; A: number } {
+ assert(
+ pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined,
+ 'color space conversion requires all of R, G and B components'
+ );
+
+ let rgbVec = [pixel.R, pixel.G, pixel.B];
+ rgbVec = lin_P3(rgbVec);
+ let rgbMatrix = [[rgbVec[0]], [rgbVec[1]], [rgbVec[2]]];
+ rgbMatrix = XYZ_to_lin_sRGB(lin_P3_to_XYZ(rgbMatrix));
+ rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]];
+ rgbVec = gam_sRGB(rgbVec);
+
+ pixel.R = rgbVec[0];
+ pixel.G = rgbVec[1];
+ pixel.B = rgbVec[2];
+
+ return pixel;
+}
+/**
+ * @returns the converted pixels in `{R: number, G: number, B: number, A: number}`.
+ *
+ * Follow conversion steps in CSS Color Module Level 4
+ * https://drafts.csswg.org/css-color/#predefined-to-predefined
+ * display-p3 and sRGB share the same white points.
+ */
+export function srgbToDisplayP3(pixel: {
+ R: number;
+ G: number;
+ B: number;
+ A: number;
+}): { R: number; G: number; B: number; A: number } {
+ assert(
+ pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined,
+ 'color space conversion requires all of R, G and B components'
+ );
+
+ let rgbVec = [pixel.R, pixel.G, pixel.B];
+ rgbVec = lin_sRGB(rgbVec);
+ let rgbMatrix = [[rgbVec[0]], [rgbVec[1]], [rgbVec[2]]];
+ rgbMatrix = XYZ_to_lin_P3(lin_sRGB_to_XYZ(rgbMatrix));
+ rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]];
+ rgbVec = gam_P3(rgbVec);
+
+ pixel.R = rgbVec[0];
+ pixel.G = rgbVec[1];
+ pixel.B = rgbVec[2];
+
+ return pixel;
+}
+
+type InPlaceColorConversion = (rgba: {
+ R: number;
+ G: number;
+ B: number;
+ readonly A: number; // Alpha never changes during a conversion.
+}) => void;
+
+/**
+ * Returns a function which applies the specified colorspace/premultiplication conversion.
+ * Does not clamp, so may return values outside of the `dstColorSpace` gamut, due to either
+ * color space conversion or alpha premultiplication.
+ */
+export function makeInPlaceColorConversion({
+ srcPremultiplied,
+ dstPremultiplied,
+ srcColorSpace = 'srgb',
+ dstColorSpace = 'srgb',
+}: {
+ srcPremultiplied: boolean;
+ dstPremultiplied: boolean;
+ srcColorSpace?: PredefinedColorSpace;
+ dstColorSpace?: PredefinedColorSpace;
+}): InPlaceColorConversion {
+ const requireColorSpaceConversion = srcColorSpace !== dstColorSpace;
+ const requireUnpremultiplyAlpha =
+ srcPremultiplied && (requireColorSpaceConversion || srcPremultiplied !== dstPremultiplied);
+ const requirePremultiplyAlpha =
+ dstPremultiplied && (requireColorSpaceConversion || srcPremultiplied !== dstPremultiplied);
+
+ return rgba => {
+ assert(rgba.A >= 0.0 && rgba.A <= 1.0, 'rgba.A out of bounds');
+
+ if (requireUnpremultiplyAlpha) {
+ if (rgba.A !== 0.0) {
+ rgba.R /= rgba.A;
+ rgba.G /= rgba.A;
+ rgba.B /= rgba.A;
+ } else {
+ assert(
+ rgba.R === 0.0 && rgba.G === 0.0 && rgba.B === 0.0 && rgba.A === 0.0,
+ 'Unpremultiply ops with alpha value 0.0 requires all channels equals to 0.0'
+ );
+ }
+ }
+ // It's possible RGB are now > 1.
+ // This technically represents colors outside the src gamut, so no clamping yet.
+
+ if (requireColorSpaceConversion) {
+ // WebGPU currently only supports dstColorSpace = 'srgb'.
+ if (srcColorSpace === 'display-p3' && dstColorSpace === 'srgb') {
+ rgba = displayP3ToSrgb(rgba);
+ } else {
+ unreachable();
+ }
+ }
+ // Now RGB may also be negative if the src gamut is larger than the dst gamut.
+
+ if (requirePremultiplyAlpha) {
+ rgba.R *= rgba.A;
+ rgba.G *= rgba.A;
+ rgba.B *= rgba.A;
+ }
+ };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/command_buffer_maker.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/command_buffer_maker.ts
new file mode 100644
index 0000000000..f52c8bb059
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/command_buffer_maker.ts
@@ -0,0 +1,85 @@
+import { ResourceState, GPUTest } from '../gpu_test.js';
+
+export const kRenderEncodeTypes = ['render pass', 'render bundle'] as const;
+export type RenderEncodeType = typeof kRenderEncodeTypes[number];
+export const kProgrammableEncoderTypes = ['compute pass', ...kRenderEncodeTypes] as const;
+export type ProgrammableEncoderType = typeof kProgrammableEncoderTypes[number];
+export const kEncoderTypes = ['non-pass', ...kProgrammableEncoderTypes] as const;
+export type EncoderType = typeof kEncoderTypes[number];
+
+// Look up the type of the encoder based on `T`. If `T` is a union, this will be too!
+type EncoderByEncoderType<T extends EncoderType> = {
+ 'non-pass': GPUCommandEncoder;
+ 'compute pass': GPUComputePassEncoder;
+ 'render pass': GPURenderPassEncoder;
+ 'render bundle': GPURenderBundleEncoder;
+}[T];
+
+/** See {@link webgpu/api/validation/validation_test.ValidationTest.createEncoder |
+ * GPUTest.createEncoder()}. */
+export class CommandBufferMaker<T extends EncoderType> {
+ /** `GPU___Encoder` for recording commands into. */
+ // Look up the type of the encoder based on `T`. If `T` is a union, this will be too!
+ readonly encoder: EncoderByEncoderType<T>;
+
+ /**
+ * Finish any passes, finish and record any bundles, and finish/return the command buffer. Any
+ * errors are ignored and the GPUCommandBuffer (which may be an error buffer) is returned.
+ */
+ readonly finish: () => GPUCommandBuffer;
+
+ /**
+ * Finish any passes, finish and record any bundles, and finish/return the command buffer.
+ * Checks for validation errors in (only) the appropriate finish call.
+ */
+ readonly validateFinish: (shouldSucceed: boolean) => GPUCommandBuffer;
+
+ /**
+ * Finish the command buffer and submit it. Checks for validation errors in either the submit or
+ * the appropriate finish call, depending on the state of a resource used in the encoding.
+ */
+ readonly validateFinishAndSubmit: (
+ shouldBeValid: boolean,
+ submitShouldSucceedIfValid: boolean
+ ) => void;
+
+ /**
+ * `validateFinishAndSubmit()` based on the state of a resource in the command encoder.
+ * - `finish()` should fail if the resource is 'invalid'.
+ * - Only `submit()` should fail if the resource is 'destroyed'.
+ */
+ readonly validateFinishAndSubmitGivenState: (resourceState: ResourceState) => void;
+
+ constructor(
+ t: GPUTest,
+ encoder: EncoderByEncoderType<EncoderType>,
+ finish: () => GPUCommandBuffer
+ ) {
+ // TypeScript introduces an intersection type here where we don't want one.
+ this.encoder = encoder as EncoderByEncoderType<T>;
+ this.finish = finish;
+
+ // Define extra methods like this, otherwise they get unbound when destructured, e.g.:
+ // const { encoder, validateFinishAndSubmit } = t.createEncoder(type);
+ // Alternatively, do not destructure, and call member functions, e.g.:
+ // const encoder = t.createEncoder(type);
+ // encoder.validateFinish(true);
+ this.validateFinish = (shouldSucceed: boolean) => {
+ return t.expectGPUError('validation', this.finish, !shouldSucceed);
+ };
+
+ this.validateFinishAndSubmit = (
+ shouldBeValid: boolean,
+ submitShouldSucceedIfValid: boolean
+ ) => {
+ const commandBuffer = this.validateFinish(shouldBeValid);
+ if (shouldBeValid) {
+ t.expectValidationError(() => t.queue.submit([commandBuffer]), !submitShouldSucceedIfValid);
+ }
+ };
+
+ this.validateFinishAndSubmitGivenState = (resourceState: ResourceState) => {
+ this.validateFinishAndSubmit(resourceState !== 'invalid', resourceState !== 'destroyed');
+ };
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts
new file mode 100644
index 0000000000..93fa55303f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/compare.ts
@@ -0,0 +1,282 @@
+import { getIsBuildingDataCache } from '../../common/framework/data_cache.js';
+import { Colors } from '../../common/util/colors.js';
+import {
+ deserializeExpectation,
+ SerializedExpectation,
+ serializeExpectation,
+} from '../shader/execution/expression/case_cache.js';
+import { Expectation, toComparator } from '../shader/execution/expression/expression.js';
+
+import { isFloatValue, Scalar, Value, Vector } from './conversion.js';
+import { F32Interval } from './f32_interval.js';
+
+/** Comparison describes the result of a Comparator function. */
+export interface Comparison {
+ matched: boolean; // True if the two values were considered a match
+ got: string; // The string representation of the 'got' value (possibly with markup)
+ expected: string; // The string representation of the 'expected' value (possibly with markup)
+}
+
+/** Comparator is a function that compares whether the provided value matches an expectation. */
+export interface Comparator {
+ (got: Value): Comparison;
+}
+
+/**
+ * compares 'got' Value to 'expected' Value, returning the Comparison information.
+ * @param got the Value obtained from the test
+ * @param expected the expected Value
+ * @returns the comparison results
+ */
+function compareValue(got: Value, expected: Value): Comparison {
+ {
+ // Check types
+ const gTy = got.type;
+ const eTy = expected.type;
+ const bothFloatTypes = isFloatValue(got) && isFloatValue(expected);
+ if (gTy !== eTy && !bothFloatTypes) {
+ return {
+ matched: false,
+ got: `${Colors.red(gTy.toString())}(${got})`,
+ expected: `${Colors.red(eTy.toString())}(${expected})`,
+ };
+ }
+ }
+
+ if (got instanceof Scalar) {
+ const g = got;
+ const e = expected as Scalar;
+ const isFloat = g.type.kind === 'f64' || g.type.kind === 'f32' || g.type.kind === 'f16';
+ const matched =
+ (isFloat && (g.value as number) === (e.value as number)) || (!isFloat && g.value === e.value);
+ return {
+ matched,
+ got: g.toString(),
+ expected: matched ? Colors.green(e.toString()) : Colors.red(e.toString()),
+ };
+ }
+
+ if (got instanceof Vector) {
+ const gLen = got.elements.length;
+ const eLen = (expected as Vector).elements.length;
+ let matched = gLen === eLen;
+ const gElements = new Array<string>(gLen);
+ const eElements = new Array<string>(eLen);
+ for (let i = 0; i < Math.max(gLen, eLen); i++) {
+ if (i < gLen && i < eLen) {
+ const g = got.elements[i];
+ const e = (expected as Vector).elements[i];
+ const cmp = compare(g, e);
+ matched = matched && cmp.matched;
+ gElements[i] = cmp.got;
+ eElements[i] = cmp.expected;
+ continue;
+ }
+ matched = false;
+ if (i < gLen) {
+ gElements[i] = got.elements[i].toString();
+ }
+ if (i < eLen) {
+ eElements[i] = (expected as Vector).elements[i].toString();
+ }
+ }
+ return {
+ matched,
+ got: `${got.type}(${gElements.join(', ')})`,
+ expected: `${expected.type}(${eElements.join(', ')})`,
+ };
+ }
+ throw new Error(`unhandled type '${typeof got}`);
+}
+
+/**
+ * Tests it a 'got' Value is contained in 'expected' interval, returning the Comparison information.
+ * @param got the Value obtained from the test
+ * @param expected the expected F32Interval
+ * @returns the comparison results
+ */
+function compareInterval(got: Value, expected: F32Interval): Comparison {
+ {
+ // Check type
+ const gTy = got.type;
+ if (!isFloatValue(got)) {
+ return {
+ matched: false,
+ got: `${Colors.red(gTy.toString())}(${got})`,
+ expected: `floating point value`,
+ };
+ }
+ }
+
+ if (got instanceof Scalar) {
+ const g = got.value as number;
+ const matched = expected.contains(g);
+ return {
+ matched,
+ got: g.toString(),
+ expected: matched ? Colors.green(expected.toString()) : Colors.red(expected.toString()),
+ };
+ }
+
+ // Vector results are currently not handled
+ throw new Error(`unhandled type '${typeof got}`);
+}
+
+/**
+ * Tests it a 'got' Value is contained in 'expected' vector, returning the Comparison information.
+ * @param got the Value obtained from the test, is expected to be a Vector
+ * @param expected the expected array of F32Intervals, one for each element of the vector
+ * @returns the comparison results
+ */
+function compareVector(got: Value, expected: F32Interval[]): Comparison {
+ // Check got type
+ if (!(got instanceof Vector)) {
+ return {
+ matched: false,
+ got: `${Colors.red((typeof got).toString())}(${got})`,
+ expected: `Vector`,
+ };
+ }
+
+ // Check element type
+ {
+ const gTy = got.type.elementType;
+ if (!isFloatValue(got.elements[0])) {
+ return {
+ matched: false,
+ got: `${Colors.red(gTy.toString())}(${got})`,
+ expected: `floating point elements`,
+ };
+ }
+ }
+
+ if (got.elements.length !== expected.length) {
+ return {
+ matched: false,
+ got: `Vector of ${got.elements.length} elements`,
+ expected: `${expected.length} elements`,
+ };
+ }
+
+ const results = got.elements.map((_, idx) => {
+ const g = got.elements[idx].value as number;
+ return { match: expected[idx].contains(g), index: idx };
+ });
+
+ const failures = results.filter(v => !v.match).map(v => v.index);
+ if (failures.length !== 0) {
+ const expected_string = expected.map((v, idx) =>
+ idx in failures ? Colors.red(`[${v}]`) : Colors.green(`[${v}]`)
+ );
+ return {
+ matched: false,
+ got: `[${got.elements}]`,
+ expected: `[${expected_string}]`,
+ };
+ }
+
+ return {
+ matched: true,
+ got: `[${got.elements}]`,
+ expected: `[${Colors.green(expected.toString())}]`,
+ };
+}
+
+/**
+ * compare() compares 'got' to 'expected', returning the Comparison information.
+ * @param got the result obtained from the test
+ * @param expected the expected result
+ * @returns the comparison results
+ */
+export function compare(got: Value, expected: Value | F32Interval | F32Interval[]): Comparison {
+ if (expected instanceof Array) {
+ return compareVector(got, expected);
+ }
+
+ if (expected instanceof F32Interval) {
+ return compareInterval(got, expected);
+ }
+
+ return compareValue(got, expected);
+}
+
+/** @returns a Comparator that checks whether a test value matches any of the provided options */
+export function anyOf(
+ ...expectations: Expectation[]
+): Comparator | (Comparator & SerializedComparator) {
+ const comparator = (got: Value) => {
+ const failed = new Set<string>();
+ for (const e of expectations) {
+ const cmp = toComparator(e)(got);
+ if (cmp.matched) {
+ return cmp;
+ }
+ failed.add(cmp.expected);
+ }
+ return { matched: false, got: got.toString(), expected: [...failed].join(' or ') };
+ };
+
+ if (getIsBuildingDataCache()) {
+ // If there's an active DataCache, and it supports storing, then append the
+ // comparator kind and serialized expectations to the comparator, so it can
+ // be serialized.
+ comparator.kind = 'anyOf';
+ comparator.data = expectations.map(e => serializeExpectation(e));
+ }
+ return comparator;
+}
+
+/** @returns a Comparator that skips the test if the expectation is undefined */
+export function skipUndefined(
+ expectation: Expectation | undefined
+): Comparator | (Comparator & SerializedComparator) {
+ const comparator = (got: Value) => {
+ if (expectation !== undefined) {
+ return toComparator(expectation)(got);
+ }
+ return { matched: true, got: got.toString(), expected: `Treating 'undefined' as Any` };
+ };
+
+ if (getIsBuildingDataCache()) {
+ // If there's an active DataCache, and it supports storing, then append the
+ // comparator kind and serialized expectations to the comparator, so it can
+ // be serialized.
+ comparator.kind = 'skipUndefined';
+ if (expectation !== undefined) {
+ comparator.data = serializeExpectation(expectation);
+ }
+ }
+ return comparator;
+}
+
+/** SerializedComparatorAnyOf is the serialized type of an `anyOf` comparator. */
+type SerializedComparatorAnyOf = {
+ kind: 'anyOf';
+ data: SerializedExpectation[];
+};
+
+/** SerializedComparatorSkipUndefined is the serialized type of an `skipUndefined` comparator. */
+type SerializedComparatorSkipUndefined = {
+ kind: 'skipUndefined';
+ data?: SerializedExpectation;
+};
+
+/** SerializedComparator is a union of all the possible serialized comparator types. */
+export type SerializedComparator = SerializedComparatorAnyOf | SerializedComparatorSkipUndefined;
+
+/**
+ * Deserializes a comparator from a SerializedComparator.
+ * @param data the SerializedComparator
+ * @returns the deserialized Comparator.
+ */
+export function deserializeComparator(data: SerializedComparator): Comparator {
+ switch (data.kind) {
+ case 'anyOf': {
+ return anyOf(...data.data.map(e => deserializeExpectation(e)));
+ }
+ case 'skipUndefined': {
+ return skipUndefined(data.data !== undefined ? deserializeExpectation(data.data) : undefined);
+ }
+ }
+ throw `unhandled comparator kind`;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts
new file mode 100644
index 0000000000..6ca825ce92
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/constants.ts
@@ -0,0 +1,587 @@
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+
+// MAINTENANCE_TODO(sarahM0): Perhaps instead of kBit and kValue tables we could have one table
+// where every value is a Scalar instead of either bits or value?
+// Then tests wouldn't need most of the Scalar.fromX calls,
+// and you would probably need fewer table entries in total
+// (since each Scalar has both bits and value).
+export const kBit = {
+ // Limits of int32
+ i32: {
+ positive: {
+ min: 0x0000_0000, // 0
+ max: 0x7fff_ffff, // 2147483647
+ },
+ negative: {
+ min: 0x8000_0000, // -2147483648
+ max: 0x0000_0000, // 0
+ },
+ },
+
+ // Limits of uint32
+ u32: {
+ min: 0x0000_0000,
+ max: 0xffff_ffff,
+ },
+
+ // Limits of f32
+ f32: {
+ positive: {
+ min: 0x0080_0000,
+ max: 0x7f7f_ffff,
+ zero: 0x0000_0000,
+ nearest_max: 0x7f7f_fffe,
+ less_than_one: 0x3f7f_ffff,
+ pi: {
+ whole: 0x404_90fdb,
+ three_quarters: 0x4016_cbe4,
+ half: 0x3fc9_0fdb,
+ third: 0x3f86_0a92,
+ quarter: 0x3f49_0fdb,
+ sixth: 0x3f06_0a92,
+ },
+ e: 0x402d_f854,
+ },
+ negative: {
+ max: 0x8080_0000,
+ min: 0xff7f_ffff,
+ zero: 0x8000_0000,
+ nearest_min: 0xff7f_fffe,
+ less_than_one: 0xbf7f_ffff,
+ pi: {
+ whole: 0xc04_90fdb,
+ three_quarters: 0xc016_cbe4,
+ half: 0xbfc90fdb,
+ third: 0xbf860a92,
+ quarter: 0xbf49_0fdb,
+ sixth: 0xbf06_0a92,
+ },
+ },
+ subnormal: {
+ positive: {
+ min: 0x0000_0001,
+ max: 0x007f_ffff,
+ },
+ negative: {
+ max: 0x8000_0001,
+ min: 0x807f_ffff,
+ },
+ },
+ nan: {
+ negative: {
+ s: 0xff80_0001,
+ q: 0xffc0_0001,
+ },
+ positive: {
+ s: 0x7f80_0001,
+ q: 0x7fc0_0001,
+ },
+ },
+ infinity: {
+ positive: 0x7f80_0000,
+ negative: 0xff80_0000,
+ },
+ },
+
+ // Limits of f16
+ f16: {
+ positive: {
+ min: 0x0400,
+ max: 0x7bff,
+ zero: 0x0000,
+ },
+ negative: {
+ max: 0x8400,
+ min: 0xfbff,
+ zero: 0x8000,
+ },
+ subnormal: {
+ positive: {
+ min: 0x0001,
+ max: 0x03ff,
+ },
+ negative: {
+ max: 0x8001,
+ min: 0x83ff,
+ },
+ },
+ nan: {
+ negative: {
+ s: 0xfc01,
+ q: 0xfe01,
+ },
+ positive: {
+ s: 0x7c01,
+ q: 0x7e01,
+ },
+ },
+ infinity: {
+ positive: 0x7c00,
+ negative: 0xfc00,
+ },
+ },
+
+ // 32-bit representation of power(2, n) n = {-31, ..., 31}
+ // A uint32 representation as a JS `number`
+ // {toMinus31, ..., to31} ie. {-31, ..., 31}
+ powTwo: {
+ toMinus1: 0x3f00_0000,
+ toMinus2: 0x3e80_0000,
+ toMinus3: 0x3e00_0000,
+ toMinus4: 0x3d80_0000,
+ toMinus5: 0x3d00_0000,
+ toMinus6: 0x3c80_0000,
+ toMinus7: 0x3c00_0000,
+ toMinus8: 0x3b80_0000,
+ toMinus9: 0x3b00_0000,
+ toMinus10: 0x3a80_0000,
+ toMinus11: 0x3a00_0000,
+ toMinus12: 0x3980_0000,
+ toMinus13: 0x3900_0000,
+ toMinus14: 0x3880_0000,
+ toMinus15: 0x3800_0000,
+ toMinus16: 0x3780_0000,
+ toMinus17: 0x3700_0000,
+ toMinus18: 0x3680_0000,
+ toMinus19: 0x3600_0000,
+ toMinus20: 0x3580_0000,
+ toMinus21: 0x3500_0000,
+ toMinus22: 0x3480_0000,
+ toMinus23: 0x3400_0000,
+ toMinus24: 0x3380_0000,
+ toMinus25: 0x3300_0000,
+ toMinus26: 0x3280_0000,
+ toMinus27: 0x3200_0000,
+ toMinus28: 0x3180_0000,
+ toMinus29: 0x3100_0000,
+ toMinus30: 0x3080_0000,
+ toMinus31: 0x3000_0000,
+
+ to0: 0x0000_0001,
+ to1: 0x0000_0002,
+ to2: 0x0000_0004,
+ to3: 0x0000_0008,
+ to4: 0x0000_0010,
+ to5: 0x0000_0020,
+ to6: 0x0000_0040,
+ to7: 0x0000_0080,
+ to8: 0x0000_0100,
+ to9: 0x0000_0200,
+ to10: 0x0000_0400,
+ to11: 0x0000_0800,
+ to12: 0x0000_1000,
+ to13: 0x0000_2000,
+ to14: 0x0000_4000,
+ to15: 0x0000_8000,
+ to16: 0x0001_0000,
+ to17: 0x0002_0000,
+ to18: 0x0004_0000,
+ to19: 0x0008_0000,
+ to20: 0x0010_0000,
+ to21: 0x0020_0000,
+ to22: 0x0040_0000,
+ to23: 0x0080_0000,
+ to24: 0x0100_0000,
+ to25: 0x0200_0000,
+ to26: 0x0400_0000,
+ to27: 0x0800_0000,
+ to28: 0x1000_0000,
+ to29: 0x2000_0000,
+ to30: 0x4000_0000,
+ to31: 0x8000_0000,
+ },
+
+ // 32-bit representation of of -1 * power(2, n) n = {-31, ..., 31}
+ // An int32 represented as a JS `number`
+ // {toMinus31, ..., to31} ie. {-31, ..., 31}
+ negPowTwo: {
+ toMinus1: 0xbf00_0000,
+ toMinus2: 0xbe80_0000,
+ toMinus3: 0xbe00_0000,
+ toMinus4: 0xbd80_0000,
+ toMinus5: 0xbd00_0000,
+ toMinus6: 0xbc80_0000,
+ toMinus7: 0xbc00_0000,
+ toMinus8: 0xbb80_0000,
+ toMinus9: 0xbb00_0000,
+ toMinus10: 0xba80_0000,
+ toMinus11: 0xba00_0000,
+ toMinus12: 0xb980_0000,
+ toMinus13: 0xb900_0000,
+ toMinus14: 0xb880_0000,
+ toMinus15: 0xb800_0000,
+ toMinus16: 0xb780_0000,
+ toMinus17: 0xb700_0000,
+ toMinus18: 0xb680_0000,
+ toMinus19: 0xb600_0000,
+ toMinus20: 0xb580_0000,
+ toMinus21: 0xb500_0000,
+ toMinus22: 0xb480_0000,
+ toMinus23: 0xb400_0000,
+ toMinus24: 0xb380_0000,
+ toMinus25: 0xb300_0000,
+ toMinus26: 0xb280_0000,
+ toMinus27: 0xb200_0000,
+ toMinus28: 0xb180_0000,
+ toMinus29: 0xb100_0000,
+ toMinus30: 0xb080_0000,
+ toMinus31: 0xb000_0000,
+
+ to0: 0xffff_ffff,
+ to1: 0xffff_fffe,
+ to2: 0xffff_fffc,
+ to3: 0xffff_fff8,
+ to4: 0xffff_fff0,
+ to5: 0xffff_ffe0,
+ to6: 0xffff_ffc0,
+ to7: 0xffff_ff80,
+ to8: 0xffff_ff00,
+ to9: 0xffff_fe00,
+ to10: 0xffff_fc00,
+ to11: 0xffff_f800,
+ to12: 0xffff_f000,
+ to13: 0xffff_e000,
+ to14: 0xffff_c000,
+ to15: 0xffff_8000,
+ to16: 0xffff_0000,
+ to17: 0xfffe_0000,
+ to18: 0xfffc_0000,
+ to19: 0xfff8_0000,
+ to20: 0xfff0_0000,
+ to21: 0xffe0_0000,
+ to22: 0xffc0_0000,
+ to23: 0xff80_0000,
+ to24: 0xff00_0000,
+ to25: 0xfe00_0000,
+ to26: 0xfc00_0000,
+ to27: 0xf800_0000,
+ to28: 0xf000_0000,
+ to29: 0xe000_0000,
+ to30: 0xc000_0000,
+ to31: 0x8000_0000,
+ },
+} as const;
+
+/**
+ * Converts a 64-bit hex value to a 64-bit float value
+ *
+ * Using a locally defined function here to avoid compile time dependency
+ * issues.
+ * */
+function hexToF64(hex: bigint): number {
+ return new Float64Array(new BigInt64Array([hex]).buffer)[0];
+}
+
+/**
+ * Converts a 64-bit float value to a 64-bit hex value
+ *
+ * Using a locally defined function here to avoid compile time dependency
+ * issues.
+ * */
+function f64ToHex(number: number): bigint {
+ return new BigUint64Array(new Float64Array([number]).buffer)[0];
+}
+
+/**
+ * Converts a 32-bit hex value to a 32-bit float value
+ *
+ * Using a locally defined function here to avoid compile time dependency
+ * issues.
+ * */
+function hexToF32(hex: number): number {
+ return new Float32Array(new Uint32Array([hex]).buffer)[0];
+}
+
+/**
+ * Converts a 16-bit hex value to a 16-bit float value
+ *
+ * Using a locally defined function here to avoid compile time dependency
+ * issues.
+ * */
+function hexToF16(hex: number): number {
+ return new Float16Array(new Uint16Array([hex]).buffer)[0];
+}
+
+export const kValue = {
+ // Limits of i32
+ i32: {
+ positive: {
+ min: 0,
+ max: 2147483647,
+ },
+ negative: {
+ min: -2147483648,
+ max: 0,
+ },
+ },
+
+ // Limits of u32
+ u32: {
+ min: 0,
+ max: 4294967295,
+ },
+
+ // Limits of f32
+ f32: {
+ positive: {
+ min: hexToF32(kBit.f32.positive.min),
+ max: hexToF32(kBit.f32.positive.max),
+ nearest_max: hexToF32(kBit.f32.positive.nearest_max),
+ less_than_one: hexToF32(kBit.f32.positive.less_than_one),
+ pi: {
+ whole: hexToF32(kBit.f32.positive.pi.whole),
+ three_quarters: hexToF32(kBit.f32.positive.pi.three_quarters),
+ half: hexToF32(kBit.f32.positive.pi.half),
+ third: hexToF32(kBit.f32.positive.pi.third),
+ quarter: hexToF32(kBit.f32.positive.pi.quarter),
+ sixth: hexToF32(kBit.f32.positive.pi.sixth),
+ },
+ e: hexToF32(kBit.f32.positive.e),
+ first_f64_not_castable: hexToF32(kBit.f32.positive.max) / 2 + 2 ** 127, // mid point of 2**128 and largest f32
+ last_f64_castable: hexToF64(
+ f64ToHex(hexToF32(kBit.f32.positive.max) / 2 + 2 ** 127) - BigInt(1)
+ ), // first_f64_not_castable minus one fraction bit of the 64 bit float representation
+ },
+ negative: {
+ max: hexToF32(kBit.f32.negative.max),
+ min: hexToF32(kBit.f32.negative.min),
+ nearest_min: hexToF32(kBit.f32.negative.nearest_min),
+ less_than_one: hexToF32(kBit.f32.negative.less_than_one), // -0.999999940395
+ pi: {
+ whole: hexToF32(kBit.f32.negative.pi.whole),
+ three_quarters: hexToF32(kBit.f32.negative.pi.three_quarters),
+ half: hexToF32(kBit.f32.negative.pi.half),
+ third: hexToF32(kBit.f32.negative.pi.third),
+ quarter: hexToF32(kBit.f32.negative.pi.quarter),
+ sixth: hexToF32(kBit.f32.negative.pi.sixth),
+ },
+ first_f64_not_castable: -(hexToF32(kBit.f32.positive.max) / 2 + 2 ** 127), // mid point of -2**128 and largest f32
+ last_f64_castable: -hexToF64(
+ f64ToHex(hexToF32(kBit.f32.positive.max) / 2 + 2 ** 127) - BigInt(1)
+ ), // first_f64_not_castable minus one fraction bit of the 64 bit float representation
+ },
+ subnormal: {
+ positive: {
+ min: hexToF32(kBit.f32.subnormal.positive.min),
+ max: hexToF32(kBit.f32.subnormal.positive.max),
+ },
+ negative: {
+ max: hexToF32(kBit.f32.subnormal.negative.max),
+ min: hexToF32(kBit.f32.subnormal.negative.min),
+ },
+ },
+ infinity: {
+ positive: hexToF32(kBit.f32.infinity.positive),
+ negative: hexToF32(kBit.f32.infinity.negative),
+ },
+ },
+
+ // Limits of i16
+ i16: {
+ positive: {
+ min: 0,
+ max: 32767,
+ },
+ negative: {
+ min: -32768,
+ max: 0,
+ },
+ },
+
+ // Limits of u16
+ u16: {
+ min: 0,
+ max: 65535,
+ },
+
+ // Limits of f16
+ f16: {
+ positive: {
+ min: hexToF16(kBit.f16.positive.min),
+ max: hexToF16(kBit.f16.positive.max),
+ zero: hexToF16(kBit.f16.positive.zero),
+ first_f64_not_castable: hexToF16(kBit.f16.positive.max) / 2 + 2 ** 16, // mid point of 2**16 and largest f16
+ last_f64_castable: hexToF64(
+ f64ToHex(hexToF16(kBit.f16.positive.max) / 2 + 2 ** 16) - BigInt(1)
+ ), // first_f64_not_castable minus one fraction bit of the 64 bit float representation
+ },
+ negative: {
+ max: hexToF16(kBit.f16.negative.max),
+ min: hexToF16(kBit.f16.negative.min),
+ zero: hexToF16(kBit.f16.negative.zero),
+ first_f64_not_castable: -(hexToF16(kBit.f16.positive.max) / 2 + 2 ** 16), // mid point of -2**16 and largest f16
+ last_f64_castable: -hexToF64(
+ f64ToHex(hexToF16(kBit.f16.positive.max) / 2 + 2 ** 16) - BigInt(1)
+ ), // first_f64_not_castable minus one fraction bit of the 64 bit float representation
+ },
+ subnormal: {
+ positive: {
+ min: hexToF16(kBit.f16.subnormal.positive.min),
+ max: hexToF16(kBit.f16.subnormal.positive.max),
+ },
+ negative: {
+ max: hexToF16(kBit.f16.subnormal.negative.max),
+ min: hexToF16(kBit.f16.subnormal.negative.min),
+ },
+ },
+ infinity: {
+ positive: hexToF16(kBit.f16.infinity.positive),
+ negative: hexToF16(kBit.f16.infinity.negative),
+ },
+ },
+
+ powTwo: {
+ to0: Math.pow(2, 0),
+ to1: Math.pow(2, 1),
+ to2: Math.pow(2, 2),
+ to3: Math.pow(2, 3),
+ to4: Math.pow(2, 4),
+ to5: Math.pow(2, 5),
+ to6: Math.pow(2, 6),
+ to7: Math.pow(2, 7),
+ to8: Math.pow(2, 8),
+ to9: Math.pow(2, 9),
+ to10: Math.pow(2, 10),
+ to11: Math.pow(2, 11),
+ to12: Math.pow(2, 12),
+ to13: Math.pow(2, 13),
+ to14: Math.pow(2, 14),
+ to15: Math.pow(2, 15),
+ to16: Math.pow(2, 16),
+ to17: Math.pow(2, 17),
+ to18: Math.pow(2, 18),
+ to19: Math.pow(2, 19),
+ to20: Math.pow(2, 20),
+ to21: Math.pow(2, 21),
+ to22: Math.pow(2, 22),
+ to23: Math.pow(2, 23),
+ to24: Math.pow(2, 24),
+ to25: Math.pow(2, 25),
+ to26: Math.pow(2, 26),
+ to27: Math.pow(2, 27),
+ to28: Math.pow(2, 28),
+ to29: Math.pow(2, 29),
+ to30: Math.pow(2, 30),
+ to31: Math.pow(2, 31),
+ to32: Math.pow(2, 32),
+
+ toMinus1: Math.pow(2, -1),
+ toMinus2: Math.pow(2, -2),
+ toMinus3: Math.pow(2, -3),
+ toMinus4: Math.pow(2, -4),
+ toMinus5: Math.pow(2, -5),
+ toMinus6: Math.pow(2, -6),
+ toMinus7: Math.pow(2, -7),
+ toMinus8: Math.pow(2, -8),
+ toMinus9: Math.pow(2, -9),
+ toMinus10: Math.pow(2, -10),
+ toMinus11: Math.pow(2, -11),
+ toMinus12: Math.pow(2, -12),
+ toMinus13: Math.pow(2, -13),
+ toMinus14: Math.pow(2, -14),
+ toMinus15: Math.pow(2, -15),
+ toMinus16: Math.pow(2, -16),
+ toMinus17: Math.pow(2, -17),
+ toMinus18: Math.pow(2, -18),
+ toMinus19: Math.pow(2, -19),
+ toMinus20: Math.pow(2, -20),
+ toMinus21: Math.pow(2, -21),
+ toMinus22: Math.pow(2, -22),
+ toMinus23: Math.pow(2, -23),
+ toMinus24: Math.pow(2, -24),
+ toMinus25: Math.pow(2, -25),
+ toMinus26: Math.pow(2, -26),
+ toMinus27: Math.pow(2, -27),
+ toMinus28: Math.pow(2, -28),
+ toMinus29: Math.pow(2, -29),
+ toMinus30: Math.pow(2, -30),
+ toMinus31: Math.pow(2, -31),
+ toMinus32: Math.pow(2, -32),
+ },
+ negPowTwo: {
+ to0: -Math.pow(2, 0),
+ to1: -Math.pow(2, 1),
+ to2: -Math.pow(2, 2),
+ to3: -Math.pow(2, 3),
+ to4: -Math.pow(2, 4),
+ to5: -Math.pow(2, 5),
+ to6: -Math.pow(2, 6),
+ to7: -Math.pow(2, 7),
+ to8: -Math.pow(2, 8),
+ to9: -Math.pow(2, 9),
+ to10: -Math.pow(2, 10),
+ to11: -Math.pow(2, 11),
+ to12: -Math.pow(2, 12),
+ to13: -Math.pow(2, 13),
+ to14: -Math.pow(2, 14),
+ to15: -Math.pow(2, 15),
+ to16: -Math.pow(2, 16),
+ to17: -Math.pow(2, 17),
+ to18: -Math.pow(2, 18),
+ to19: -Math.pow(2, 19),
+ to20: -Math.pow(2, 20),
+ to21: -Math.pow(2, 21),
+ to22: -Math.pow(2, 22),
+ to23: -Math.pow(2, 23),
+ to24: -Math.pow(2, 24),
+ to25: -Math.pow(2, 25),
+ to26: -Math.pow(2, 26),
+ to27: -Math.pow(2, 27),
+ to28: -Math.pow(2, 28),
+ to29: -Math.pow(2, 29),
+ to30: -Math.pow(2, 30),
+ to31: -Math.pow(2, 31),
+ to32: -Math.pow(2, 32),
+
+ toMinus1: -Math.pow(2, -1),
+ toMinus2: -Math.pow(2, -2),
+ toMinus3: -Math.pow(2, -3),
+ toMinus4: -Math.pow(2, -4),
+ toMinus5: -Math.pow(2, -5),
+ toMinus6: -Math.pow(2, -6),
+ toMinus7: -Math.pow(2, -7),
+ toMinus8: -Math.pow(2, -8),
+ toMinus9: -Math.pow(2, -9),
+ toMinus10: -Math.pow(2, -10),
+ toMinus11: -Math.pow(2, -11),
+ toMinus12: -Math.pow(2, -12),
+ toMinus13: -Math.pow(2, -13),
+ toMinus14: -Math.pow(2, -14),
+ toMinus15: -Math.pow(2, -15),
+ toMinus16: -Math.pow(2, -16),
+ toMinus17: -Math.pow(2, -17),
+ toMinus18: -Math.pow(2, -18),
+ toMinus19: -Math.pow(2, -19),
+ toMinus20: -Math.pow(2, -20),
+ toMinus21: -Math.pow(2, -21),
+ toMinus22: -Math.pow(2, -22),
+ toMinus23: -Math.pow(2, -23),
+ toMinus24: -Math.pow(2, -24),
+ toMinus25: -Math.pow(2, -25),
+ toMinus26: -Math.pow(2, -26),
+ toMinus27: -Math.pow(2, -27),
+ toMinus28: -Math.pow(2, -28),
+ toMinus29: -Math.pow(2, -29),
+ toMinus30: -Math.pow(2, -30),
+ toMinus31: -Math.pow(2, -31),
+ toMinus32: -Math.pow(2, -32),
+ },
+
+ // Limits of i8
+ i8: {
+ positive: {
+ min: 0,
+ max: 127,
+ },
+ negative: {
+ min: -128,
+ max: 0,
+ },
+ },
+
+ // Limits of u8
+ u8: {
+ min: 0,
+ max: 255,
+ },
+} as const;
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts
new file mode 100644
index 0000000000..d81f22defe
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/conversion.ts
@@ -0,0 +1,1076 @@
+import { Colors } from '../../common/util/colors.js';
+import { assert, TypedArrayBufferView, unreachable } from '../../common/util/util.js';
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+
+import { kBit } from './constants.js';
+import {
+ cartesianProduct,
+ clamp,
+ correctlyRoundedF16,
+ isFiniteF16,
+ isSubnormalNumberF16,
+ isSubnormalNumberF32,
+} from './math.js';
+
+/**
+ * Encodes a JS `number` into a "normalized" (unorm/snorm) integer representation with `bits` bits.
+ * Input must be between -1 and 1 if signed, or 0 and 1 if unsigned.
+ *
+ * MAINTENANCE_TODO: See if performance of texel_data improves if this function is pre-specialized
+ * for a particular `bits`/`signed`.
+ */
+export function floatAsNormalizedInteger(float: number, bits: number, signed: boolean): number {
+ if (signed) {
+ assert(float >= -1 && float <= 1, () => `${float} out of bounds of snorm`);
+ const max = Math.pow(2, bits - 1) - 1;
+ return Math.round(float * max);
+ } else {
+ assert(float >= 0 && float <= 1, () => `${float} out of bounds of unorm`);
+ const max = Math.pow(2, bits) - 1;
+ return Math.round(float * max);
+ }
+}
+
+/**
+ * Decodes a JS `number` from a "normalized" (unorm/snorm) integer representation with `bits` bits.
+ * Input must be an integer in the range of the specified unorm/snorm type.
+ */
+export function normalizedIntegerAsFloat(integer: number, bits: number, signed: boolean): number {
+ assert(Number.isInteger(integer));
+ if (signed) {
+ const max = Math.pow(2, bits - 1) - 1;
+ assert(integer >= -max - 1 && integer <= max);
+ if (integer === -max - 1) {
+ integer = -max;
+ }
+ return integer / max;
+ } else {
+ const max = Math.pow(2, bits) - 1;
+ assert(integer >= 0 && integer <= max);
+ return integer / max;
+ }
+}
+
+/**
+ * Encodes a JS `number` into an IEEE754 floating point number with the specified number of
+ * sign, exponent, mantissa bits, and exponent bias.
+ * Returns the result as an integer-valued JS `number`.
+ *
+ * Does not handle clamping, overflow, or denormal inputs.
+ * On underflow (result is subnormal), rounds to (signed) zero.
+ *
+ * MAINTENANCE_TODO: Replace usages of this with numberToFloatBits.
+ */
+export function float32ToFloatBits(
+ n: number,
+ signBits: 0 | 1,
+ exponentBits: number,
+ mantissaBits: number,
+ bias: number
+): number {
+ assert(exponentBits <= 8);
+ assert(mantissaBits <= 23);
+ assert(Number.isFinite(n));
+
+ if (n === 0) {
+ return 0;
+ }
+
+ if (signBits === 0) {
+ assert(n >= 0);
+ }
+
+ const buf = new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT));
+ buf.setFloat32(0, n, true);
+ const bits = buf.getUint32(0, true);
+ // bits (32): seeeeeeeefffffffffffffffffffffff
+
+ const mantissaBitsToDiscard = 23 - mantissaBits;
+
+ // 0 or 1
+ const sign = (bits >> 31) & signBits;
+
+ // >> to remove mantissa, & to remove sign, - 127 to remove bias.
+ const exp = ((bits >> 23) & 0xff) - 127;
+
+ // Convert to the new biased exponent.
+ const newBiasedExp = bias + exp;
+ assert(newBiasedExp < 1 << exponentBits, () => `input number ${n} overflows target type`);
+
+ if (newBiasedExp <= 0) {
+ // Result is subnormal or zero. Round to (signed) zero.
+ return sign << (exponentBits + mantissaBits);
+ } else {
+ // Mask only the mantissa, and discard the lower bits.
+ const newMantissa = (bits & 0x7fffff) >> mantissaBitsToDiscard;
+ return (sign << (exponentBits + mantissaBits)) | (newBiasedExp << mantissaBits) | newMantissa;
+ }
+}
+
+/**
+ * Encodes a JS `number` into an IEEE754 16 bit floating point number.
+ * Returns the result as an integer-valued JS `number`.
+ *
+ * Does not handle clamping, overflow, or denormal inputs.
+ * On underflow (result is subnormal), rounds to (signed) zero.
+ */
+export function float32ToFloat16Bits(n: number) {
+ return float32ToFloatBits(n, 1, 5, 10, 15);
+}
+
+/**
+ * Decodes an IEEE754 16 bit floating point number into a JS `number` and returns.
+ */
+export function float16BitsToFloat32(float16Bits: number): number {
+ return floatBitsToNumber(float16Bits, kFloat16Format);
+}
+
+type FloatFormat = { signed: 0 | 1; exponentBits: number; mantissaBits: number; bias: number };
+
+/** FloatFormat defining IEEE754 32-bit float. */
+export const kFloat32Format = { signed: 1, exponentBits: 8, mantissaBits: 23, bias: 127 } as const;
+/** FloatFormat defining IEEE754 16-bit float. */
+export const kFloat16Format = { signed: 1, exponentBits: 5, mantissaBits: 10, bias: 15 } as const;
+
+/**
+ * Once-allocated ArrayBuffer/views to avoid overhead of allocation when converting between numeric formats
+ *
+ * workingData* is shared between multiple functions in this file, so to avoid re-entrancy problems, make sure in
+ * functions that use it that they don't call themselves or other functions that use workingData*.
+ */
+const workingData = new ArrayBuffer(4);
+const workingDataU32 = new Uint32Array(workingData);
+const workingDataU16 = new Uint16Array(workingData);
+const workingDataU8 = new Uint8Array(workingData);
+const workingDataF32 = new Float32Array(workingData);
+const workingDataF16 = new Float16Array(workingData);
+const workingDataI16 = new Int16Array(workingData);
+const workingDataI8 = new Int8Array(workingData);
+
+/** Bitcast u32 (represented as integer Number) to f32 (represented as floating-point Number). */
+export function float32BitsToNumber(bits: number): number {
+ workingDataU32[0] = bits;
+ return workingDataF32[0];
+}
+/** Bitcast f32 (represented as floating-point Number) to u32 (represented as integer Number). */
+export function numberToFloat32Bits(number: number): number {
+ workingDataF32[0] = number;
+ return workingDataU32[0];
+}
+
+/**
+ * Decodes an IEEE754 float with the supplied format specification into a JS number.
+ *
+ * The format MUST be no larger than a 32-bit float.
+ */
+export function floatBitsToNumber(bits: number, fmt: FloatFormat): number {
+ // Pad the provided bits out to f32, then convert to a `number` with the wrong bias.
+ // E.g. for f16 to f32:
+ // - f16: S EEEEE MMMMMMMMMM
+ // ^ 000^^^^^ ^^^^^^^^^^0000000000000
+ // - f32: S eeeEEEEE MMMMMMMMMMmmmmmmmmmmmmm
+
+ const kNonSignBits = fmt.exponentBits + fmt.mantissaBits;
+ const kNonSignBitsMask = (1 << kNonSignBits) - 1;
+ const expAndMantBits = bits & kNonSignBitsMask;
+ let f32BitsWithWrongBias = expAndMantBits << (kFloat32Format.mantissaBits - fmt.mantissaBits);
+ f32BitsWithWrongBias |= (bits << (31 - kNonSignBits)) & 0x8000_0000;
+ const numberWithWrongBias = float32BitsToNumber(f32BitsWithWrongBias);
+ return numberWithWrongBias * 2 ** (kFloat32Format.bias - fmt.bias);
+}
+
+/**
+ * Encodes a JS `number` into an IEEE754 floating point number with the specified format.
+ * Returns the result as an integer-valued JS `number`.
+ *
+ * Does not handle clamping, overflow, or denormal inputs.
+ * On underflow (result is subnormal), rounds to (signed) zero.
+ */
+export function numberToFloatBits(number: number, fmt: FloatFormat): number {
+ return float32ToFloatBits(number, fmt.signed, fmt.exponentBits, fmt.mantissaBits, fmt.bias);
+}
+
+/**
+ * Given a floating point number (as an integer representing its bits), computes how many ULPs it is
+ * from zero.
+ *
+ * Subnormal numbers are skipped, so that 0 is one ULP from the minimum normal number.
+ * Subnormal values are flushed to 0.
+ * Positive and negative 0 are both considered to be 0 ULPs from 0.
+ */
+export function floatBitsToNormalULPFromZero(bits: number, fmt: FloatFormat): number {
+ const mask_sign = fmt.signed << (fmt.exponentBits + fmt.mantissaBits);
+ const mask_expt = ((1 << fmt.exponentBits) - 1) << fmt.mantissaBits;
+ const mask_mant = (1 << fmt.mantissaBits) - 1;
+ const mask_rest = mask_expt | mask_mant;
+
+ assert(fmt.exponentBits + fmt.mantissaBits <= 31);
+
+ const sign = bits & mask_sign ? -1 : 1;
+ const rest = bits & mask_rest;
+ const subnormal_or_zero = (bits & mask_expt) === 0;
+ const infinity_or_nan = (bits & mask_expt) === mask_expt;
+ assert(!infinity_or_nan, 'no ulp representation for infinity/nan');
+
+ // The first normal number is mask_mant+1, so subtract mask_mant to make min_normal - zero = 1ULP.
+ const abs_ulp_from_zero = subnormal_or_zero ? 0 : rest - mask_mant;
+ return sign * abs_ulp_from_zero;
+}
+
+/**
+ * Encodes three JS `number` values into RGB9E5, returned as an integer-valued JS `number`.
+ *
+ * RGB9E5 represents three partial-precision floating-point numbers encoded into a single 32-bit
+ * value all sharing the same 5-bit exponent.
+ * There is no sign bit, and there is a shared 5-bit biased (15) exponent and a 9-bit
+ * mantissa for each channel. The mantissa does NOT have an implicit leading "1.",
+ * and instead has an implicit leading "0.".
+ */
+export function packRGB9E5UFloat(r: number, g: number, b: number): number {
+ for (const v of [r, g, b]) {
+ assert(v >= 0 && v < Math.pow(2, 16));
+ }
+
+ const buf = new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT));
+ const extractMantissaAndExponent = (n: number) => {
+ const mantissaBits = 9;
+ buf.setFloat32(0, n, true);
+ const bits = buf.getUint32(0, true);
+ // >> to remove mantissa, & to remove sign
+ let biasedExponent = (bits >> 23) & 0xff;
+ const mantissaBitsToDiscard = 23 - mantissaBits;
+ let mantissa = (bits & 0x7fffff) >> mantissaBitsToDiscard;
+
+ // RGB9E5UFloat has an implicit leading 0. instead of a leading 1.,
+ // so we need to move the 1. into the mantissa and bump the exponent.
+ // For float32 encoding, the leading 1 is only present if the biased
+ // exponent is non-zero.
+ if (biasedExponent !== 0) {
+ mantissa = (mantissa >> 1) | 0b100000000;
+ biasedExponent += 1;
+ }
+ return { biasedExponent, mantissa };
+ };
+
+ const { biasedExponent: rExp, mantissa: rOrigMantissa } = extractMantissaAndExponent(r);
+ const { biasedExponent: gExp, mantissa: gOrigMantissa } = extractMantissaAndExponent(g);
+ const { biasedExponent: bExp, mantissa: bOrigMantissa } = extractMantissaAndExponent(b);
+
+ // Use the largest exponent, and shift the mantissa accordingly
+ const exp = Math.max(rExp, gExp, bExp);
+ const rMantissa = rOrigMantissa >> (exp - rExp);
+ const gMantissa = gOrigMantissa >> (exp - gExp);
+ const bMantissa = bOrigMantissa >> (exp - bExp);
+
+ const bias = 15;
+ const biasedExp = exp === 0 ? 0 : exp - 127 + bias;
+ assert(biasedExp >= 0 && biasedExp <= 31);
+ return rMantissa | (gMantissa << 9) | (bMantissa << 18) | (biasedExp << 27);
+}
+
+/**
+ * Quantizes two f32s to f16 and then packs them in a u32
+ *
+ * This should implement the same behaviour as the builtin `pack2x16float` from
+ * WGSL.
+ *
+ * Caller is responsible to ensuring inputs are f32s
+ *
+ * @param x first f32 to be packed
+ * @param y second f32 to be packed
+ * @returns an array of possible results for pack2x16float. Elements are either
+ * a number or undefined.
+ * undefined indicates that any value is valid, since the input went
+ * out of bounds.
+ */
+export function pack2x16float(x: number, y: number): (number | undefined)[] {
+ // Generates all possible valid u16 bit fields for a given f32 to f16 conversion.
+ // Assumes FTZ for both the f32 and f16 value is allowed.
+ const generateU16s = (n: number): number[] => {
+ let contains_subnormals = isSubnormalNumberF32(n);
+ const n_f16s = correctlyRoundedF16(n);
+ contains_subnormals ||= n_f16s.some(isSubnormalNumberF16);
+
+ const n_u16s = n_f16s.map(f16 => {
+ workingDataF16[0] = f16;
+ return workingDataU16[0];
+ });
+
+ const contains_poszero = n_u16s.some(u => u === kBit.f16.positive.zero);
+ const contains_negzero = n_u16s.some(u => u === kBit.f16.negative.zero);
+ if (!contains_negzero && (contains_poszero || contains_subnormals)) {
+ n_u16s.push(kBit.f16.negative.zero);
+ }
+
+ if (!contains_poszero && (contains_negzero || contains_subnormals)) {
+ n_u16s.push(kBit.f16.positive.zero);
+ }
+
+ return n_u16s;
+ };
+
+ if (!isFiniteF16(x) || !isFiniteF16(y)) {
+ // This indicates any value is valid, so it isn't worth bothering
+ // calculating the more restrictive possibilities.
+ return [undefined];
+ }
+
+ const results = new Array<number>();
+ for (const p of cartesianProduct(generateU16s(x), generateU16s(y))) {
+ assert(p.length === 2, 'cartesianProduct of 2 arrays returned an entry with not 2 elements');
+ workingDataU16[0] = p[0];
+ workingDataU16[1] = p[1];
+ results.push(workingDataU32[0]);
+ }
+
+ return results;
+}
+
+/**
+ * Converts two normalized f32s to i16s and then packs them in a u32
+ *
+ * This should implement the same behaviour as the builtin `pack2x16snorm` from
+ * WGSL.
+ *
+ * Caller is responsible to ensuring inputs are normalized f32s
+ *
+ * @param x first f32 to be packed
+ * @param y second f32 to be packed
+ * @returns a number that is expected result of pack2x16snorm.
+ */
+export function pack2x16snorm(x: number, y: number): number {
+ // Converts f32 to i16 via the pack2x16snorm formula.
+ // FTZ is not explicitly handled, because all subnormals will produce a value
+ // between 0 and 1, but significantly away from the edges, so floor goes to 0.
+ const generateI16 = (n: number): number => {
+ return Math.floor(0.5 + 32767 * Math.min(1, Math.max(-1, n)));
+ };
+
+ workingDataI16[0] = generateI16(x);
+ workingDataI16[1] = generateI16(y);
+
+ return workingDataU32[0];
+}
+
+/**
+ * Converts two normalized f32s to u16s and then packs them in a u32
+ *
+ * This should implement the same behaviour as the builtin `pack2x16unorm` from
+ * WGSL.
+ *
+ * Caller is responsible to ensuring inputs are normalized f32s
+ *
+ * @param x first f32 to be packed
+ * @param y second f32 to be packed
+ * @returns an number that is expected result of pack2x16unorm.
+ */
+export function pack2x16unorm(x: number, y: number): number {
+ // Converts f32 to u16 via the pack2x16unorm formula.
+ // FTZ is not explicitly handled, because all subnormals will produce a value
+ // between 0.5 and much less than 1, so floor goes to 0.
+ const generateU16 = (n: number): number => {
+ return Math.floor(0.5 + 65535 * Math.min(1, Math.max(0, n)));
+ };
+
+ workingDataU16[0] = generateU16(x);
+ workingDataU16[1] = generateU16(y);
+
+ return workingDataU32[0];
+}
+
+/**
+ * Converts four normalized f32s to i8s and then packs them in a u32
+ *
+ * This should implement the same behaviour as the builtin `pack4x8snorm` from
+ * WGSL.
+ *
+ * Caller is responsible to ensuring inputs are normalized f32s
+ *
+ * @param vals four f32s to be packed
+ * @returns a number that is expected result of pack4x8usorm.
+ */
+export function pack4x8snorm(...vals: [number, number, number, number]): number {
+ // Converts f32 to u8 via the pack4x8snorm formula.
+ // FTZ is not explicitly handled, because all subnormals will produce a value
+ // between 0 and 1, so floor goes to 0.
+ const generateI8 = (n: number): number => {
+ return Math.floor(0.5 + 127 * Math.min(1, Math.max(-1, n)));
+ };
+
+ for (const idx in vals) {
+ workingDataI8[idx] = generateI8(vals[idx]);
+ }
+
+ return workingDataU32[0];
+}
+
+/**
+ * Converts four normalized f32s to u8s and then packs them in a u32
+ *
+ * This should implement the same behaviour as the builtin `pack4x8unorm` from
+ * WGSL.
+ *
+ * Caller is responsible to ensuring inputs are normalized f32s
+ *
+ * @param vals four f32s to be packed
+ * @returns a number that is expected result of pack4x8unorm.
+ */
+export function pack4x8unorm(...vals: [number, number, number, number]): number {
+ // Converts f32 to u8 via the pack4x8unorm formula.
+ // FTZ is not explicitly handled, because all subnormals will produce a value
+ // between 0.5 and much less than 1, so floor goes to 0.
+ const generateU8 = (n: number): number => {
+ return Math.floor(0.5 + 255 * Math.min(1, Math.max(0, n)));
+ };
+
+ for (const idx in vals) {
+ workingDataU8[idx] = generateU8(vals[idx]);
+ }
+
+ return workingDataU32[0];
+}
+
+/**
+ * Asserts that a number is within the representable (inclusive) of the integer type with the
+ * specified number of bits and signedness.
+ *
+ * MAINTENANCE_TODO: Assert isInteger? Then this function "asserts that a number is representable"
+ * by the type.
+ */
+export function assertInIntegerRange(n: number, bits: number, signed: boolean): void {
+ if (signed) {
+ const min = -Math.pow(2, bits - 1);
+ const max = Math.pow(2, bits - 1) - 1;
+ assert(n >= min && n <= max);
+ } else {
+ const max = Math.pow(2, bits) - 1;
+ assert(n >= 0 && n <= max);
+ }
+}
+
+/**
+ * Converts a linear value into a "gamma"-encoded value using the sRGB-clamped transfer function.
+ */
+export function gammaCompress(n: number): number {
+ n = n <= 0.0031308 ? (323 * n) / 25 : (211 * Math.pow(n, 5 / 12) - 11) / 200;
+ return clamp(n, { min: 0, max: 1 });
+}
+
+/**
+ * Converts a "gamma"-encoded value into a linear value using the sRGB-clamped transfer function.
+ */
+export function gammaDecompress(n: number): number {
+ n = n <= 0.04045 ? (n * 25) / 323 : Math.pow((200 * n + 11) / 211, 12 / 5);
+ return clamp(n, { min: 0, max: 1 });
+}
+
+/** Converts a 32-bit float value to a 32-bit unsigned integer value */
+export function float32ToUint32(f32: number): number {
+ const f32Arr = new Float32Array(1);
+ f32Arr[0] = f32;
+ const u32Arr = new Uint32Array(f32Arr.buffer);
+ return u32Arr[0];
+}
+
+/** Converts a 32-bit unsigned integer value to a 32-bit float value */
+export function uint32ToFloat32(u32: number): number {
+ const u32Arr = new Uint32Array(1);
+ u32Arr[0] = u32;
+ const f32Arr = new Float32Array(u32Arr.buffer);
+ return f32Arr[0];
+}
+
+/** Converts a 32-bit float value to a 32-bit signed integer value */
+export function float32ToInt32(f32: number): number {
+ const f32Arr = new Float32Array(1);
+ f32Arr[0] = f32;
+ const i32Arr = new Int32Array(f32Arr.buffer);
+ return i32Arr[0];
+}
+
+/** Converts a 32-bit unsigned integer value to a 32-bit signed integer value */
+export function uint32ToInt32(u32: number): number {
+ const u32Arr = new Uint32Array(1);
+ u32Arr[0] = u32;
+ const i32Arr = new Int32Array(u32Arr.buffer);
+ return i32Arr[0];
+}
+
+/** Converts a 16-bit float value to a 16-bit unsigned integer value */
+export function float16ToUint16(f16: number): number {
+ const f16Arr = new Float16Array(1);
+ f16Arr[0] = f16;
+ const u16Arr = new Uint16Array(f16Arr.buffer);
+ return u16Arr[0];
+}
+
+/** Converts a 16-bit unsigned integer value to a 16-bit float value */
+export function uint16ToFloat16(u16: number): number {
+ const u16Arr = new Uint16Array(1);
+ u16Arr[0] = u16;
+ const f16Arr = new Float16Array(u16Arr.buffer);
+ return f16Arr[0];
+}
+
+/** Converts a 16-bit float value to a 16-bit signed integer value */
+export function float16ToInt16(f16: number): number {
+ const f16Arr = new Float16Array(1);
+ f16Arr[0] = f16;
+ const i16Arr = new Int16Array(f16Arr.buffer);
+ return i16Arr[0];
+}
+
+/** A type of number representable by Scalar. */
+export type ScalarKind =
+ | 'f64'
+ | 'f32'
+ | 'f16'
+ | 'u32'
+ | 'u16'
+ | 'u8'
+ | 'i32'
+ | 'i16'
+ | 'i8'
+ | 'bool';
+
+/** ScalarType describes the type of WGSL Scalar. */
+export class ScalarType {
+ readonly kind: ScalarKind; // The named type
+ readonly _size: number; // In bytes
+ readonly read: (buf: Uint8Array, offset: number) => Scalar; // reads a scalar from a buffer
+
+ constructor(kind: ScalarKind, size: number, read: (buf: Uint8Array, offset: number) => Scalar) {
+ this.kind = kind;
+ this._size = size;
+ this.read = read;
+ }
+
+ public toString(): string {
+ return this.kind;
+ }
+
+ public get size(): number {
+ return this._size;
+ }
+}
+
+/** ScalarType describes the type of WGSL Vector. */
+export class VectorType {
+ readonly width: number; // Number of elements in the vector
+ readonly elementType: ScalarType; // Element type
+
+ constructor(width: number, elementType: ScalarType) {
+ this.width = width;
+ this.elementType = elementType;
+ }
+
+ /**
+ * @returns a vector constructed from the values read from the buffer at the
+ * given byte offset
+ */
+ public read(buf: Uint8Array, offset: number): Vector {
+ const elements: Array<Scalar> = [];
+ for (let i = 0; i < this.width; i++) {
+ elements[i] = this.elementType.read(buf, offset);
+ offset += this.elementType.size;
+ }
+ return new Vector(elements);
+ }
+
+ public toString(): string {
+ return `vec${this.width}<${this.elementType}>`;
+ }
+
+ public get size(): number {
+ return this.elementType.size * this.width;
+ }
+}
+
+// Maps a string representation of a vector type to vector type.
+const vectorTypes = new Map<string, VectorType>();
+
+export function TypeVec(width: number, elementType: ScalarType): VectorType {
+ const key = `${elementType.toString()} ${width}}`;
+ let ty = vectorTypes.get(key);
+ if (ty !== undefined) {
+ return ty;
+ }
+ ty = new VectorType(width, elementType);
+ vectorTypes.set(key, ty);
+ return ty;
+}
+
+/** Type is a ScalarType or VectorType. */
+export type Type = ScalarType | VectorType;
+
+export const TypeI32 = new ScalarType('i32', 4, (buf: Uint8Array, offset: number) =>
+ i32(new Int32Array(buf.buffer, offset)[0])
+);
+export const TypeU32 = new ScalarType('u32', 4, (buf: Uint8Array, offset: number) =>
+ u32(new Uint32Array(buf.buffer, offset)[0])
+);
+export const TypeF64 = new ScalarType('f64', 8, (buf: Uint8Array, offset: number) =>
+ f32(new Float64Array(buf.buffer, offset)[0])
+);
+export const TypeF32 = new ScalarType('f32', 4, (buf: Uint8Array, offset: number) =>
+ f32(new Float32Array(buf.buffer, offset)[0])
+);
+export const TypeI16 = new ScalarType('i16', 2, (buf: Uint8Array, offset: number) =>
+ i16(new Int16Array(buf.buffer, offset)[0])
+);
+export const TypeU16 = new ScalarType('u16', 2, (buf: Uint8Array, offset: number) =>
+ u16(new Uint16Array(buf.buffer, offset)[0])
+);
+export const TypeF16 = new ScalarType('f16', 2, (buf: Uint8Array, offset: number) =>
+ f16Bits(new Uint16Array(buf.buffer, offset)[0])
+);
+export const TypeI8 = new ScalarType('i8', 1, (buf: Uint8Array, offset: number) =>
+ i8(new Int8Array(buf.buffer, offset)[0])
+);
+export const TypeU8 = new ScalarType('u8', 1, (buf: Uint8Array, offset: number) =>
+ u8(new Uint8Array(buf.buffer, offset)[0])
+);
+export const TypeBool = new ScalarType('bool', 4, (buf: Uint8Array, offset: number) =>
+ bool(new Uint32Array(buf.buffer, offset)[0] !== 0)
+);
+
+/** @returns the ScalarType from the ScalarKind */
+export function scalarType(kind: ScalarKind): ScalarType {
+ switch (kind) {
+ case 'f64':
+ return TypeF64;
+ case 'f32':
+ return TypeF32;
+ case 'f16':
+ return TypeF16;
+ case 'u32':
+ return TypeU32;
+ case 'u16':
+ return TypeU16;
+ case 'u8':
+ return TypeU8;
+ case 'i32':
+ return TypeI32;
+ case 'i16':
+ return TypeI16;
+ case 'i8':
+ return TypeI8;
+ case 'bool':
+ return TypeBool;
+ }
+}
+
+/** @returns the number of scalar (element) types of the given Type */
+export function numElementsOf(ty: Type): number {
+ if (ty instanceof ScalarType) {
+ return 1;
+ }
+ if (ty instanceof VectorType) {
+ return ty.width;
+ }
+ throw new Error(`unhandled type ${ty}`);
+}
+
+/** @returns the scalar (element) type of the given Type */
+export function scalarTypeOf(ty: Type): ScalarType {
+ if (ty instanceof ScalarType) {
+ return ty;
+ }
+ if (ty instanceof VectorType) {
+ return ty.elementType;
+ }
+ throw new Error(`unhandled type ${ty}`);
+}
+
+/** ScalarValue is the JS type that can be held by a Scalar */
+type ScalarValue = boolean | number;
+
+/** Class that encapsulates a single scalar value of various types. */
+export class Scalar {
+ readonly value: ScalarValue; // The scalar value
+ readonly type: ScalarType; // The type of the scalar
+ readonly bits: Uint8Array; // The scalar value packed in a Uint8Array
+
+ public constructor(type: ScalarType, value: ScalarValue, bits: TypedArrayBufferView) {
+ this.value = value;
+ this.type = type;
+ this.bits = new Uint8Array(bits.buffer);
+ }
+
+ /**
+ * Copies the scalar value to the Uint8Array buffer at the provided byte offset.
+ * @param buffer the destination buffer
+ * @param offset the byte offset within buffer
+ */
+ public copyTo(buffer: Uint8Array, offset: number) {
+ for (let i = 0; i < this.bits.length; i++) {
+ buffer[offset + i] = this.bits[i];
+ }
+ }
+
+ /**
+ * @returns the WGSL representation of this scalar value
+ */
+ public wgsl(): string {
+ const withPoint = (x: number) => {
+ const str = `${x}`;
+ return str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`;
+ };
+ if (isFinite(this.value as number)) {
+ switch (this.type.kind) {
+ case 'f32':
+ return `${withPoint(this.value as number)}f`;
+ case 'f16':
+ return `${withPoint(this.value as number)}h`;
+ case 'u32':
+ return `${this.value}u`;
+ case 'i32':
+ return `i32(${this.value})`;
+ case 'bool':
+ return `${this.value}`;
+ }
+ }
+ throw new Error(
+ `scalar of value ${this.value} and type ${this.type} has no WGSL representation`
+ );
+ }
+
+ public toString(): string {
+ if (this.type.kind === 'bool') {
+ return Colors.bold(this.value.toString());
+ }
+ switch (this.value) {
+ case Infinity:
+ case -Infinity:
+ return Colors.bold(this.value.toString());
+ default: {
+ // Uint8Array.map returns a Uint8Array, so cannot use .map directly
+ const hex = Array.from(this.bits)
+ .reverse()
+ .map(x => x.toString(16).padStart(2, '0'))
+ .join('');
+ const n = this.value as Number;
+ if (n !== null && isFloatValue(this)) {
+ let str = this.value.toString();
+ str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`;
+ return isSubnormalNumberF32(n.valueOf())
+ ? `${Colors.bold(str)} (0x${hex} subnormal)`
+ : `${Colors.bold(str)} (0x${hex})`;
+ }
+ return `${Colors.bold(this.value.toString())} (0x${hex})`;
+ }
+ }
+ }
+}
+
+/** Create an f64 from a numeric value, a JS `number`. */
+export function f64(value: number): Scalar {
+ const arr = new Float64Array([value]);
+ return new Scalar(TypeF64, arr[0], arr);
+}
+/** Create an f32 from a numeric value, a JS `number`. */
+export function f32(value: number): Scalar {
+ const arr = new Float32Array([value]);
+ return new Scalar(TypeF32, arr[0], arr);
+}
+/** Create an f16 from a numeric value, a JS `number`. */
+export function f16(value: number): Scalar {
+ const arr = new Float16Array([value]);
+ return new Scalar(TypeF16, arr[0], arr);
+}
+/** Create an f32 from a bit representation, a uint32 represented as a JS `number`. */
+export function f32Bits(bits: number): Scalar {
+ const arr = new Uint32Array([bits]);
+ return new Scalar(TypeF32, new Float32Array(arr.buffer)[0], arr);
+}
+/** Create an f16 from a bit representation, a uint16 represented as a JS `number`. */
+export function f16Bits(bits: number): Scalar {
+ const arr = new Uint16Array([bits]);
+ return new Scalar(TypeF16, new Float16Array(arr.buffer)[0], arr);
+}
+
+/** Create an i32 from a numeric value, a JS `number`. */
+export function i32(value: number): Scalar {
+ const arr = new Int32Array([value]);
+ return new Scalar(TypeI32, arr[0], arr);
+}
+/** Create an i16 from a numeric value, a JS `number`. */
+export function i16(value: number): Scalar {
+ const arr = new Int16Array([value]);
+ return new Scalar(TypeI16, arr[0], arr);
+}
+/** Create an i8 from a numeric value, a JS `number`. */
+export function i8(value: number): Scalar {
+ const arr = new Int8Array([value]);
+ return new Scalar(TypeI8, arr[0], arr);
+}
+
+/** Create an i32 from a bit representation, a uint32 represented as a JS `number`. */
+export function i32Bits(bits: number): Scalar {
+ const arr = new Uint32Array([bits]);
+ return new Scalar(TypeI32, new Int32Array(arr.buffer)[0], arr);
+}
+/** Create an i16 from a bit representation, a uint16 represented as a JS `number`. */
+export function i16Bits(bits: number): Scalar {
+ const arr = new Uint16Array([bits]);
+ return new Scalar(TypeI16, new Int16Array(arr.buffer)[0], arr);
+}
+/** Create an i8 from a bit representation, a uint8 represented as a JS `number`. */
+export function i8Bits(bits: number): Scalar {
+ const arr = new Uint8Array([bits]);
+ return new Scalar(TypeI8, new Int8Array(arr.buffer)[0], arr);
+}
+
+/** Create a u32 from a numeric value, a JS `number`. */
+export function u32(value: number): Scalar {
+ const arr = new Uint32Array([value]);
+ return new Scalar(TypeU32, arr[0], arr);
+}
+/** Create a u16 from a numeric value, a JS `number`. */
+export function u16(value: number): Scalar {
+ const arr = new Uint16Array([value]);
+ return new Scalar(TypeU16, arr[0], arr);
+}
+/** Create a u8 from a numeric value, a JS `number`. */
+export function u8(value: number): Scalar {
+ const arr = new Uint8Array([value]);
+ return new Scalar(TypeU8, arr[0], arr);
+}
+
+/** Create an u32 from a bit representation, a uint32 represented as a JS `number`. */
+export function u32Bits(bits: number): Scalar {
+ const arr = new Uint32Array([bits]);
+ return new Scalar(TypeU32, bits, arr);
+}
+/** Create an u16 from a bit representation, a uint16 represented as a JS `number`. */
+export function u16Bits(bits: number): Scalar {
+ const arr = new Uint16Array([bits]);
+ return new Scalar(TypeU16, bits, arr);
+}
+/** Create an u8 from a bit representation, a uint8 represented as a JS `number`. */
+export function u8Bits(bits: number): Scalar {
+ const arr = new Uint8Array([bits]);
+ return new Scalar(TypeU8, bits, arr);
+}
+
+/** Create a boolean value. */
+export function bool(value: boolean): Scalar {
+ // WGSL does not support using 'bool' types directly in storage / uniform
+ // buffers, so instead we pack booleans in a u32, where 'false' is zero and
+ // 'true' is any non-zero value.
+ const arr = new Uint32Array([value ? 1 : 0]);
+ return new Scalar(TypeBool, value, arr);
+}
+
+/** A 'true' literal value */
+export const True = bool(true);
+
+/** A 'false' literal value */
+export const False = bool(false);
+
+export function reinterpretF32AsU32(f32: number): number {
+ const array = new Float32Array(1);
+ array[0] = f32;
+ return new Uint32Array(array.buffer)[0];
+}
+
+export function reinterpretU32AsF32(u32: number): number {
+ const array = new Uint32Array(1);
+ array[0] = u32;
+ return new Float32Array(array.buffer)[0];
+}
+
+/**
+ * Class that encapsulates a vector value.
+ */
+export class Vector {
+ readonly elements: Array<Scalar>;
+ readonly type: VectorType;
+
+ public constructor(elements: Array<Scalar>) {
+ if (elements.length < 2 || elements.length > 4) {
+ throw new Error(`vector element count must be between 2 and 4, got ${elements.length}`);
+ }
+ for (let i = 1; i < elements.length; i++) {
+ const a = elements[0].type;
+ const b = elements[i].type;
+ if (a !== b) {
+ throw new Error(
+ `cannot mix vector element types. Found elements with types '${a}' and '${b}'`
+ );
+ }
+ }
+ this.elements = elements;
+ this.type = TypeVec(elements.length, elements[0].type);
+ }
+
+ /**
+ * Copies the vector value to the Uint8Array buffer at the provided byte offset.
+ * @param buffer the destination buffer
+ * @param offset the byte offset within buffer
+ */
+ public copyTo(buffer: Uint8Array, offset: number) {
+ for (const element of this.elements) {
+ element.copyTo(buffer, offset);
+ offset += this.type.elementType.size;
+ }
+ }
+
+ /**
+ * @returns the WGSL representation of this vector value
+ */
+ public wgsl(): string {
+ const els = this.elements.map(v => v.wgsl()).join(', ');
+ return `vec${this.type.width}(${els})`;
+ }
+
+ public toString(): string {
+ return `${this.type}(${this.elements.map(e => e.toString()).join(', ')})`;
+ }
+
+ public get x() {
+ assert(0 < this.elements.length);
+ return this.elements[0];
+ }
+
+ public get y() {
+ assert(1 < this.elements.length);
+ return this.elements[1];
+ }
+
+ public get z() {
+ assert(2 < this.elements.length);
+ return this.elements[2];
+ }
+
+ public get w() {
+ assert(3 < this.elements.length);
+ return this.elements[3];
+ }
+}
+
+/** Helper for constructing a new two-element vector with the provided values */
+export function vec2(x: Scalar, y: Scalar) {
+ return new Vector([x, y]);
+}
+
+/** Helper for constructing a new three-element vector with the provided values */
+export function vec3(x: Scalar, y: Scalar, z: Scalar) {
+ return new Vector([x, y, z]);
+}
+
+/** Helper for constructing a new four-element vector with the provided values */
+export function vec4(x: Scalar, y: Scalar, z: Scalar, w: Scalar) {
+ return new Vector([x, y, z, w]);
+}
+
+/**
+ * Helper for constructing Vectors from arrays of numbers
+ *
+ * @param v array of numbers to be converted, must contain 2, 3 or 4 elements
+ * @param op function to convert from number to Scalar, e.g. 'f32`
+ */
+export function toVector(v: number[], op: (n: number) => Scalar): Vector {
+ switch (v.length) {
+ case 2:
+ return vec2(op(v[0]), op(v[1]));
+ case 3:
+ return vec3(op(v[0]), op(v[1]), op(v[2]));
+ case 4:
+ return vec4(op(v[0]), op(v[1]), op(v[2]), op(v[3]));
+ }
+ unreachable(`input to 'toVector' must contain 2, 3, or 4 elements`);
+}
+
+/** Value is a Scalar or Vector value. */
+export type Value = Scalar | Vector;
+
+export type SerializedValueScalar = {
+ kind: 'scalar';
+ type: ScalarKind;
+ value: boolean | number;
+};
+
+export type SerializedValueVector = {
+ kind: 'vector';
+ type: ScalarKind;
+ value: boolean[] | number[];
+};
+
+export type SerializedValue = SerializedValueScalar | SerializedValueVector;
+
+export function serializeValue(v: Value): SerializedValue {
+ const value = (kind: ScalarKind, s: Scalar) => {
+ switch (kind) {
+ case 'f32':
+ return new Uint32Array(s.bits.buffer)[0];
+ case 'f16':
+ return new Uint16Array(s.bits.buffer)[0];
+ default:
+ return s.value;
+ }
+ };
+ if (v instanceof Scalar) {
+ const kind = v.type.kind;
+ return {
+ kind: 'scalar',
+ type: kind,
+ value: value(kind, v),
+ };
+ }
+ if (v instanceof Vector) {
+ const kind = v.type.elementType.kind;
+ return {
+ kind: 'vector',
+ type: kind,
+ value: v.elements.map(e => value(kind, e)) as boolean[] | number[],
+ };
+ }
+ unreachable(`unhandled value type: ${v}`);
+}
+
+export function deserializeValue(data: SerializedValue): Value {
+ const buildScalar = (v: ScalarValue): Scalar => {
+ switch (data.type) {
+ case 'f64':
+ return f64(v as number);
+ case 'i32':
+ return i32(v as number);
+ case 'u32':
+ return u32(v as number);
+ case 'f32':
+ return f32Bits(v as number);
+ case 'i16':
+ return i16(v as number);
+ case 'u16':
+ return u16(v as number);
+ case 'f16':
+ return f16Bits(v as number);
+ case 'i8':
+ return i8(v as number);
+ case 'u8':
+ return u8(v as number);
+ case 'bool':
+ return bool(v as boolean);
+ default:
+ unreachable(`unhandled value type: ${data.type}`);
+ }
+ };
+ switch (data.kind) {
+ case 'scalar': {
+ return buildScalar(data.value);
+ }
+ case 'vector': {
+ return new Vector(data.value.map(v => buildScalar(v)));
+ }
+ }
+}
+
+/** @returns if the Value is a float scalar type */
+export function isFloatValue(v: Value): boolean {
+ if (v instanceof Scalar) {
+ const s = v;
+ return s.type.kind === 'f64' || s.type.kind === 'f32' || s.type.kind === 'f16';
+ }
+ return false;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/copy_to_texture.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/copy_to_texture.ts
new file mode 100644
index 0000000000..2f751ff6a2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/copy_to_texture.ts
@@ -0,0 +1,194 @@
+import { assert, memcpy } from '../../common/util/util.js';
+import { RegularTextureFormat } from '../capability_info.js';
+import { GPUTest } from '../gpu_test.js';
+import { reifyExtent3D, reifyOrigin3D } from '../util/unions.js';
+
+import { makeInPlaceColorConversion } from './color_space_conversion.js';
+import { TexelView } from './texture/texel_view.js';
+import { TexelCompareOptions, textureContentIsOKByT2B } from './texture/texture_ok.js';
+
+/**
+ * Predefined copy sub rect meta infos.
+ */
+export const kCopySubrectInfo = [
+ {
+ srcOrigin: { x: 2, y: 2 },
+ dstOrigin: { x: 0, y: 0, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 4, height: 4 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+ {
+ srcOrigin: { x: 10, y: 2 },
+ dstOrigin: { x: 0, y: 0, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 4, height: 4 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+ {
+ srcOrigin: { x: 2, y: 10 },
+ dstOrigin: { x: 0, y: 0, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 4, height: 4 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+ {
+ srcOrigin: { x: 10, y: 10 },
+ dstOrigin: { x: 0, y: 0, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 4, height: 4 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+ {
+ srcOrigin: { x: 2, y: 2 },
+ dstOrigin: { x: 2, y: 2, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 16, height: 16 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+ {
+ srcOrigin: { x: 10, y: 2 },
+ dstOrigin: { x: 2, y: 2, z: 0 },
+ srcSize: { width: 16, height: 16 },
+ dstSize: { width: 16, height: 16 },
+ copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
+ },
+] as const;
+
+export class CopyToTextureUtils extends GPUTest {
+ doFlipY(
+ sourcePixels: Uint8ClampedArray,
+ width: number,
+ height: number,
+ bytesPerPixel: number
+ ): Uint8ClampedArray {
+ const dstPixels = new Uint8ClampedArray(width * height * bytesPerPixel);
+ for (let i = 0; i < height; ++i) {
+ for (let j = 0; j < width; ++j) {
+ const srcPixelPos = i * width + j;
+ // WebGL readPixel returns pixels from bottom-left origin. Using CopyExternalImageToTexture
+ // to copy from WebGL Canvas keeps top-left origin. So the expectation from webgl.readPixel should
+ // be flipped.
+ const dstPixelPos = (height - i - 1) * width + j;
+
+ memcpy(
+ { src: sourcePixels, start: srcPixelPos * bytesPerPixel, length: bytesPerPixel },
+ { dst: dstPixels, start: dstPixelPos * bytesPerPixel }
+ );
+ }
+ }
+
+ return dstPixels;
+ }
+
+ getExpectedDstPixelsFromSrcPixels({
+ srcPixels,
+ srcOrigin,
+ srcSize,
+ dstOrigin,
+ dstSize,
+ subRectSize,
+ format,
+ flipSrcBeforeCopy,
+ srcDoFlipYDuringCopy,
+ conversion,
+ }: {
+ srcPixels: Uint8ClampedArray;
+ srcOrigin: GPUOrigin2D;
+ srcSize: GPUExtent3D;
+ dstOrigin: GPUOrigin3D;
+ dstSize: GPUExtent3D;
+ subRectSize: GPUExtent3D;
+ format: RegularTextureFormat;
+ flipSrcBeforeCopy: boolean;
+ srcDoFlipYDuringCopy: boolean;
+ conversion: {
+ srcPremultiplied: boolean;
+ dstPremultiplied: boolean;
+ srcColorSpace?: PredefinedColorSpace;
+ dstColorSpace?: PredefinedColorSpace;
+ };
+ }): TexelView {
+ const applyConversion = makeInPlaceColorConversion(conversion);
+
+ const reifySrcOrigin = reifyOrigin3D(srcOrigin);
+ const reifySrcSize = reifyExtent3D(srcSize);
+ const reifyDstOrigin = reifyOrigin3D(dstOrigin);
+ const reifyDstSize = reifyExtent3D(dstSize);
+ const reifySubRectSize = reifyExtent3D(subRectSize);
+
+ assert(
+ reifyDstOrigin.x + reifySubRectSize.width <= reifyDstSize.width &&
+ reifyDstOrigin.y + reifySubRectSize.height <= reifyDstSize.height,
+ 'subrect is out of bounds'
+ );
+
+ const divide = 255.0;
+ return TexelView.fromTexelsAsColors(
+ format,
+ coords => {
+ assert(
+ coords.x >= reifyDstOrigin.x &&
+ coords.y >= reifyDstOrigin.y &&
+ coords.x < reifyDstOrigin.x + reifySubRectSize.width &&
+ coords.y < reifyDstOrigin.y + reifySubRectSize.height &&
+ coords.z === 0,
+ 'out of bounds'
+ );
+ // Map dst coords to get candidate src pixel position in y.
+ let yInSubRect = coords.y - reifyDstOrigin.y;
+
+ // If srcDoFlipYDuringCopy is true, a flipY op has been applied to src during copy.
+ // WebGPU spec requires origin option relative to the top-left corner of the source image,
+ // increasing downward consistently.
+ // https://www.w3.org/TR/webgpu/#dom-gpuimagecopyexternalimage-flipy
+ // Flip only happens in copy rect contents and src origin always top-left.
+ // Get candidate src pixel position in y by mirroring in copy sub rect.
+ if (srcDoFlipYDuringCopy) yInSubRect = reifySubRectSize.height - 1 - yInSubRect;
+
+ let src_y = yInSubRect + reifySrcOrigin.y;
+
+ // Test might generate flipped source based on srcPixels, e.g. Create ImageBitmap based on srcPixels but set orientation to 'flipY'
+ // Get candidate src pixel position in y by mirroring in source.
+ if (flipSrcBeforeCopy) src_y = reifySrcSize.height - src_y - 1;
+
+ const pixelPos =
+ src_y * reifySrcSize.width + (coords.x - reifyDstOrigin.x) + reifySrcOrigin.x;
+
+ const rgba = {
+ R: srcPixels[pixelPos * 4] / divide,
+ G: srcPixels[pixelPos * 4 + 1] / divide,
+ B: srcPixels[pixelPos * 4 + 2] / divide,
+ A: srcPixels[pixelPos * 4 + 3] / divide,
+ };
+ applyConversion(rgba);
+ return rgba;
+ },
+ { clampToFormatRange: true }
+ );
+ }
+
+ doTestAndCheckResult(
+ imageCopyExternalImage: GPUImageCopyExternalImage,
+ dstTextureCopyView: GPUImageCopyTextureTagged,
+ expTexelView: TexelView,
+ copySize: Required<GPUExtent3DDict>,
+ texelCompareOptions: TexelCompareOptions
+ ): void {
+ this.device.queue.copyExternalImageToTexture(
+ imageCopyExternalImage,
+ dstTextureCopyView,
+ copySize
+ );
+
+ const resultPromise = textureContentIsOKByT2B(
+ this,
+ { texture: dstTextureCopyView.texture, origin: dstTextureCopyView.origin },
+ copySize,
+ { expTexelView },
+ texelCompareOptions
+ );
+ this.eventualExpectOK(resultPromise);
+ this.trackForCleanup(dstTextureCopyView.texture);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/create_elements.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/create_elements.ts
new file mode 100644
index 0000000000..927916b6e7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/create_elements.ts
@@ -0,0 +1,95 @@
+import { Fixture } from '../../common/framework/fixture.js';
+import { unreachable } from '../../common/util/util.js';
+
+// TESTING_TODO: This should expand to more canvas types (which will enhance a bunch of tests):
+// - canvas element not in dom
+// - canvas element in dom
+// - offscreen canvas from transferControlToOffscreen from canvas not in dom
+// - offscreen canvas from transferControlToOffscreen from canvas in dom
+// - offscreen canvas from new OffscreenCanvas
+export const kAllCanvasTypes = ['onscreen', 'offscreen'] as const;
+export type CanvasType = typeof kAllCanvasTypes[number];
+
+type CanvasForCanvasType<T extends CanvasType> = {
+ onscreen: HTMLCanvasElement;
+ offscreen: OffscreenCanvas;
+}[T];
+
+/** Valid contextId for HTMLCanvasElement/OffscreenCanvas,
+ * spec: https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext
+ */
+export const kValidCanvasContextIds = [
+ '2d',
+ 'bitmaprenderer',
+ 'webgl',
+ 'webgl2',
+ 'webgpu',
+] as const;
+export type CanvasContext = typeof kValidCanvasContextIds[number];
+
+/** Helper(s) to determine if context is copyable. */
+export function canCopyFromCanvasContext(contextName: CanvasContext) {
+ switch (contextName) {
+ case '2d':
+ case 'webgl':
+ case 'webgl2':
+ case 'webgpu':
+ return true;
+ default:
+ return false;
+ }
+}
+
+/** Create HTMLCanvas/OffscreenCanvas. */
+export function createCanvas<T extends CanvasType>(
+ test: Fixture,
+ canvasType: T,
+ width: number,
+ height: number
+): CanvasForCanvasType<T> {
+ if (canvasType === 'onscreen') {
+ if (typeof document !== 'undefined') {
+ return createOnscreenCanvas(test, width, height) as CanvasForCanvasType<T>;
+ } else {
+ test.skip('Cannot create HTMLCanvasElement');
+ }
+ } else if (canvasType === 'offscreen') {
+ if (typeof OffscreenCanvas !== 'undefined') {
+ return createOffscreenCanvas(test, width, height) as CanvasForCanvasType<T>;
+ } else {
+ test.skip('Cannot create an OffscreenCanvas');
+ }
+ } else {
+ unreachable();
+ }
+}
+
+/** Create HTMLCanvasElement. */
+export function createOnscreenCanvas(
+ test: Fixture,
+ width: number,
+ height: number
+): HTMLCanvasElement {
+ let canvas: HTMLCanvasElement;
+ if (typeof document !== 'undefined') {
+ canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ } else {
+ test.skip('Cannot create HTMLCanvasElement');
+ }
+ return canvas;
+}
+
+/** Create OffscreenCanvas. */
+export function createOffscreenCanvas(
+ test: Fixture,
+ width: number,
+ height: number
+): OffscreenCanvas {
+ if (typeof OffscreenCanvas === 'undefined') {
+ test.skip('OffscreenCanvas is not supported');
+ }
+
+ return new OffscreenCanvas(width, height);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts
new file mode 100644
index 0000000000..fbcbade771
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/device_pool.ts
@@ -0,0 +1,391 @@
+import { SkipTestCase } from '../../common/framework/fixture.js';
+import { attemptGarbageCollection } from '../../common/util/collect_garbage.js';
+import { getGPU } from '../../common/util/navigator_gpu.js';
+import {
+ assert,
+ raceWithRejectOnTimeout,
+ assertReject,
+ unreachable,
+} from '../../common/util/util.js';
+import { kLimitInfo, kLimits } from '../capability_info.js';
+
+export interface DeviceProvider {
+ readonly device: GPUDevice;
+ expectDeviceLost(reason: GPUDeviceLostReason): void;
+}
+
+class TestFailedButDeviceReusable extends Error {}
+class FeaturesNotSupported extends Error {}
+export class TestOOMedShouldAttemptGC extends Error {}
+
+export class DevicePool {
+ private holders: 'uninitialized' | 'failed' | DescriptorToHolderMap = 'uninitialized';
+
+ /** Acquire a device from the pool and begin the error scopes. */
+ async acquire(descriptor?: UncanonicalizedDeviceDescriptor): Promise<DeviceProvider> {
+ let errorMessage = '';
+ if (this.holders === 'uninitialized') {
+ this.holders = new DescriptorToHolderMap();
+ try {
+ await this.holders.getOrCreate(undefined);
+ } catch (ex) {
+ this.holders = 'failed';
+ if (ex instanceof Error) {
+ errorMessage = ` with ${ex.name} "${ex.message}"`;
+ }
+ }
+ }
+
+ assert(
+ this.holders !== 'failed',
+ `WebGPU device failed to initialize${errorMessage}; not retrying`
+ );
+
+ const holder = await this.holders.getOrCreate(descriptor);
+
+ assert(holder.state === 'free', 'Device was in use on DevicePool.acquire');
+ holder.state = 'acquired';
+ holder.beginTestScope();
+ return holder;
+ }
+
+ /**
+ * End the error scopes and check for errors.
+ * Then, if the device seems reusable, release it back into the pool. Otherwise, drop it.
+ */
+ async release(holder: DeviceProvider): Promise<void> {
+ assert(this.holders instanceof DescriptorToHolderMap, 'DevicePool got into a bad state');
+ assert(holder instanceof DeviceHolder, 'DeviceProvider should always be a DeviceHolder');
+
+ assert(holder.state === 'acquired', 'trying to release a device while already released');
+ try {
+ await holder.endTestScope();
+
+ // (Hopefully if the device was lost, it has been reported by the time endErrorScopes()
+ // has finished (or timed out). If not, it could cause a finite number of extra test
+ // failures following this one (but should recover eventually).)
+ assert(
+ holder.lostInfo === undefined,
+ `Device was unexpectedly lost. Reason: ${holder.lostInfo?.reason}, Message: ${holder.lostInfo?.message}`
+ );
+ } catch (ex) {
+ // Any error that isn't explicitly TestFailedButDeviceReusable forces a new device to be
+ // created for the next test.
+ if (!(ex instanceof TestFailedButDeviceReusable)) {
+ this.holders.delete(holder);
+ if ('destroy' in holder.device) {
+ holder.device.destroy();
+ }
+
+ // Release the (hopefully only) ref to the GPUDevice.
+ holder.releaseGPUDevice();
+
+ // Try to clean up, in case there are stray GPU resources in need of collection.
+ if (ex instanceof TestOOMedShouldAttemptGC) {
+ await attemptGarbageCollection();
+ }
+ }
+ // In the try block, we may throw an error if the device is lost in order to force device
+ // reinitialization, however, if the device lost was expected we want to suppress the error
+ // The device lost is expected when `holder.expectedLostReason` is equal to
+ // `holder.lostInfo.reason`.
+ const expectedDeviceLost =
+ holder.expectedLostReason !== undefined &&
+ holder.lostInfo !== undefined &&
+ holder.expectedLostReason === holder.lostInfo.reason;
+ if (!expectedDeviceLost) {
+ throw ex;
+ }
+ } finally {
+ // Mark the holder as free so the device can be reused (if it's still in this.devices).
+ holder.state = 'free';
+ }
+ }
+}
+
+/**
+ * Map from GPUDeviceDescriptor to DeviceHolder.
+ */
+class DescriptorToHolderMap {
+ /** Map keys that are known to be unsupported and can be rejected quickly. */
+ private unsupported: Set<string> = new Set();
+ private holders: Map<string, DeviceHolder> = new Map();
+
+ /** Deletes an item from the map by DeviceHolder value. */
+ delete(holder: DeviceHolder): void {
+ for (const [k, v] of this.holders) {
+ if (v === holder) {
+ this.holders.delete(k);
+ return;
+ }
+ }
+ unreachable("internal error: couldn't find DeviceHolder to delete");
+ }
+
+ /**
+ * Gets a DeviceHolder from the map if it exists; otherwise, calls create() to create one,
+ * inserts it, and returns it.
+ *
+ * If an `uncanonicalizedDescriptor` is provided, it is canonicalized and used as the map key.
+ * If one is not provided, the map key is `""` (empty string).
+ *
+ * Throws SkipTestCase if devices with this descriptor are unsupported.
+ */
+ async getOrCreate(
+ uncanonicalizedDescriptor: UncanonicalizedDeviceDescriptor | undefined
+ ): Promise<DeviceHolder> {
+ const [descriptor, key] = canonicalizeDescriptor(uncanonicalizedDescriptor);
+ // Quick-reject descriptors that are known to be unsupported already.
+ if (this.unsupported.has(key)) {
+ throw new SkipTestCase(
+ `GPUDeviceDescriptor previously failed: ${JSON.stringify(descriptor)}`
+ );
+ }
+
+ // Search for an existing device with the same descriptor.
+ {
+ const value = this.holders.get(key);
+ if (value) {
+ // Move it to the end of the Map (most-recently-used).
+ this.holders.delete(key);
+ this.holders.set(key, value);
+ return value;
+ }
+ }
+
+ // No existing item was found; add a new one.
+ let value;
+ try {
+ value = await DeviceHolder.create(descriptor);
+ } catch (ex) {
+ if (ex instanceof FeaturesNotSupported) {
+ this.unsupported.add(key);
+ throw new SkipTestCase(
+ `GPUDeviceDescriptor not supported: ${JSON.stringify(descriptor)}\n${ex?.message ?? ''}`
+ );
+ }
+
+ throw ex;
+ }
+ this.insertAndCleanUp(key, value);
+ return value;
+ }
+
+ /** Insert an entry, then remove the least-recently-used items if there are too many. */
+ private insertAndCleanUp(key: string, value: DeviceHolder) {
+ this.holders.set(key, value);
+
+ const kMaxEntries = 5;
+ if (this.holders.size > kMaxEntries) {
+ // Delete the first (least recently used) item in the set.
+ for (const [key] of this.holders) {
+ this.holders.delete(key);
+ return;
+ }
+ }
+ }
+}
+
+export type UncanonicalizedDeviceDescriptor = {
+ requiredFeatures?: Iterable<GPUFeatureName>;
+ requiredLimits?: Record<string, GPUSize32>;
+ /** @deprecated this field cannot be used */
+ nonGuaranteedFeatures?: undefined;
+ /** @deprecated this field cannot be used */
+ nonGuaranteedLimits?: undefined;
+ /** @deprecated this field cannot be used */
+ extensions?: undefined;
+ /** @deprecated this field cannot be used */
+ features?: undefined;
+};
+type CanonicalDeviceDescriptor = Omit<
+ Required<GPUDeviceDescriptor>,
+ 'label' | 'nonGuaranteedFeatures' | 'nonGuaranteedLimits'
+>;
+/**
+ * Make a stringified map-key from a GPUDeviceDescriptor.
+ * Tries to make sure all defaults are resolved, first - but it's okay if some are missed
+ * (it just means some GPUDevice objects won't get deduplicated).
+ *
+ * This does **not** canonicalize `undefined` (the "default" descriptor) into a fully-qualified
+ * GPUDeviceDescriptor. This is just because `undefined` is a common case and we want to use it
+ * as a sanity check that WebGPU is working.
+ */
+function canonicalizeDescriptor(
+ desc: UncanonicalizedDeviceDescriptor | undefined
+): [CanonicalDeviceDescriptor | undefined, string] {
+ if (desc === undefined) {
+ return [undefined, ''];
+ }
+
+ const featuresCanonicalized = desc.requiredFeatures
+ ? Array.from(new Set(desc.requiredFeatures)).sort()
+ : [];
+
+ /** Canonicalized version of the requested limits: in canonical order, with only values which are
+ * specified _and_ non-default. */
+ const limitsCanonicalized: Record<string, number> = {};
+ if (desc.requiredLimits) {
+ for (const limit of kLimits) {
+ const requestedValue = desc.requiredLimits[limit];
+ const defaultValue = kLimitInfo[limit].default;
+ // Skip adding a limit to limitsCanonicalized if it is the same as the default.
+ if (requestedValue !== undefined && requestedValue !== defaultValue) {
+ limitsCanonicalized[limit] = requestedValue;
+ }
+ }
+ }
+
+ // Type ensures every field is carried through.
+ const descriptorCanonicalized: CanonicalDeviceDescriptor = {
+ requiredFeatures: featuresCanonicalized,
+ requiredLimits: limitsCanonicalized,
+ defaultQueue: {},
+ };
+ return [descriptorCanonicalized, JSON.stringify(descriptorCanonicalized)];
+}
+
+function supportsFeature(
+ adapter: GPUAdapter,
+ descriptor: CanonicalDeviceDescriptor | undefined
+): boolean {
+ if (descriptor === undefined) {
+ return true;
+ }
+
+ for (const feature of descriptor.requiredFeatures) {
+ if (!adapter.features.has(feature)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * DeviceHolder has three states:
+ * - 'free': Free to be used for a new test.
+ * - 'acquired': In use by a running test.
+ */
+type DeviceHolderState = 'free' | 'acquired';
+
+/**
+ * Holds a GPUDevice and tracks its state (free/acquired) and handles device loss.
+ */
+class DeviceHolder implements DeviceProvider {
+ /** The device. Will be cleared during cleanup if there were unexpected errors. */
+ private _device: GPUDevice | undefined;
+ /** Whether the device is in use by a test or not. */
+ state: DeviceHolderState = 'free';
+ /** initially undefined; becomes set when the device is lost */
+ lostInfo?: GPUDeviceLostInfo;
+ /** Set if the device is expected to be lost. */
+ expectedLostReason?: GPUDeviceLostReason;
+
+ // Gets a device and creates a DeviceHolder.
+ // If the device is lost, DeviceHolder.lost gets set.
+ static async create(descriptor: CanonicalDeviceDescriptor | undefined): Promise<DeviceHolder> {
+ const gpu = getGPU();
+ const adapter = await gpu.requestAdapter();
+ assert(adapter !== null, 'requestAdapter returned null');
+ if (!supportsFeature(adapter, descriptor)) {
+ throw new FeaturesNotSupported('One or more features are not supported');
+ }
+ const device = await adapter.requestDevice(descriptor);
+ assert(device !== null, 'requestDevice returned null');
+
+ return new DeviceHolder(device);
+ }
+
+ private constructor(device: GPUDevice) {
+ this._device = device;
+ void this._device.lost.then(ev => {
+ this.lostInfo = ev;
+ });
+ }
+
+ get device() {
+ assert(this._device !== undefined);
+ return this._device;
+ }
+
+ /** Push error scopes that surround test execution. */
+ beginTestScope(): void {
+ assert(this.state === 'acquired');
+ this.device.pushErrorScope('out-of-memory');
+ this.device.pushErrorScope('validation');
+ }
+
+ /** Mark the DeviceHolder as expecting a device loss when the test scope ends. */
+ expectDeviceLost(reason: GPUDeviceLostReason) {
+ assert(this.state === 'acquired');
+ this.expectedLostReason = reason;
+ }
+
+ /**
+ * Attempt to end test scopes: Check that there are no extra error scopes, and that no
+ * otherwise-uncaptured errors occurred during the test. Time out if it takes too long.
+ */
+ endTestScope(): Promise<void> {
+ assert(this.state === 'acquired');
+ const kTimeout = 5000;
+
+ // Time out if attemptEndTestScope (popErrorScope or onSubmittedWorkDone) never completes. If
+ // this rejects, the device won't be reused, so it's OK that popErrorScope calls may not have
+ // finished.
+ //
+ // This could happen due to a browser bug - e.g.,
+ // as of this writing, on Chrome GPU process crash, popErrorScope just hangs.
+ return raceWithRejectOnTimeout(this.attemptEndTestScope(), kTimeout, 'endTestScope timed out');
+ }
+
+ private async attemptEndTestScope(): Promise<void> {
+ let gpuValidationError: GPUError | null;
+ let gpuOutOfMemoryError: GPUError | null;
+
+ // Submit to the queue to attempt to force a GPU flush.
+ this.device.queue.submit([]);
+
+ try {
+ // May reject if the device was lost.
+ [gpuValidationError, gpuOutOfMemoryError] = await Promise.all([
+ this.device.popErrorScope(),
+ this.device.popErrorScope(),
+ ]);
+ } catch (ex) {
+ assert(this.lostInfo !== undefined, 'popErrorScope failed; did beginTestScope get missed?');
+ throw ex;
+ }
+
+ // Attempt to wait for the queue to be idle.
+ if (this.device.queue.onSubmittedWorkDone) {
+ await this.device.queue.onSubmittedWorkDone();
+ }
+
+ await assertReject(
+ this.device.popErrorScope(),
+ 'There was an extra error scope on the stack after a test'
+ );
+
+ if (gpuValidationError !== null) {
+ assert(gpuValidationError instanceof GPUValidationError);
+ // Allow the device to be reused.
+ throw new TestFailedButDeviceReusable(
+ `Unexpected validation error occurred: ${gpuValidationError.message}`
+ );
+ }
+ if (gpuOutOfMemoryError !== null) {
+ assert(gpuOutOfMemoryError instanceof GPUOutOfMemoryError);
+ // Don't allow the device to be reused; unexpected OOM could break the device.
+ throw new TestOOMedShouldAttemptGC('Unexpected out-of-memory error occurred');
+ }
+ }
+
+ /**
+ * Release the ref to the GPUDevice. This should be the only ref held by the DevicePool or
+ * GPUTest, so in theory it can get garbage collected.
+ */
+ releaseGPUDevice(): void {
+ this._device = undefined;
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/f32_interval.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/f32_interval.ts
new file mode 100644
index 0000000000..f74ff543cb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/f32_interval.ts
@@ -0,0 +1,2136 @@
+import { assert, unreachable } from '../../common/util/util.js';
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+
+import { kValue } from './constants.js';
+import { reinterpretF32AsU32, reinterpretU32AsF32 } from './conversion.js';
+import {
+ calculatePermutations,
+ cartesianProduct,
+ correctlyRoundedF16,
+ correctlyRoundedF32,
+ flushSubnormalNumberF32,
+ isFiniteF16,
+ isFiniteF32,
+ isSubnormalNumberF16,
+ isSubnormalNumberF32,
+ oneULP,
+} from './math.js';
+
+/**
+ * Representation of bounds for an interval as an array with either one or two
+ * elements. Single element indicates that the interval is a single point. For
+ * two elements, the first is the lower bound of the interval and the second is
+ * the upper bound.
+ */
+export type IntervalBounds = [number] | [number, number];
+
+/** Represents a closed interval in the f32 range */
+export class F32Interval {
+ public readonly begin: number;
+ public readonly end: number;
+ private static _any: F32Interval;
+
+ /** Constructor
+ *
+ * `toF32Interval` is the preferred way to create F32Intervals
+ *
+ * @param bounds either a pair of numbers indicating the beginning then the
+ * end of the interval, or a single element array indicating the
+ * interval is a point
+ */
+ public constructor(...bounds: IntervalBounds) {
+ const [begin, end] = bounds.length === 2 ? bounds : [bounds[0], bounds[0]];
+ assert(!Number.isNaN(begin) && !Number.isNaN(end), `bounds need to be non-NaN`);
+ assert(begin <= end, `bounds[0] (${begin}) must be less than or equal to bounds[1] (${end})`);
+
+ this.begin = begin;
+ this.end = end;
+ }
+
+ /** @returns begin and end if non-point interval, otherwise just begin */
+ public bounds(): IntervalBounds {
+ return this.isPoint() ? [this.begin] : [this.begin, this.end];
+ }
+
+ /** @returns if a point or interval is completely contained by this interval */
+ public contains(n: number | F32Interval): boolean {
+ if (Number.isNaN(n)) {
+ // Being the any interval indicates that accuracy is not defined for this
+ // test, so the test is just checking that this input doesn't cause the
+ // implementation to misbehave, so NaN is accepted.
+ return this.begin === Number.NEGATIVE_INFINITY && this.end === Number.POSITIVE_INFINITY;
+ }
+ const i = toF32Interval(n);
+ return this.begin <= i.begin && this.end >= i.end;
+ }
+
+ /** @returns if any values in the interval may be flushed to zero, this
+ * includes any subnormals and zero itself.
+ */
+ public containsZeroOrSubnormals(): boolean {
+ return !(
+ this.end < kValue.f32.subnormal.negative.min || this.begin > kValue.f32.subnormal.positive.max
+ );
+ }
+
+ /** @returns if this interval contains a single point */
+ public isPoint(): boolean {
+ return this.begin === this.end;
+ }
+
+ /** @returns if this interval only contains f32 finite values */
+ public isFinite(): boolean {
+ return isFiniteF32(this.begin) && isFiniteF32(this.end);
+ }
+
+ /** @returns an interval with the tightest bounds that includes all provided intervals */
+ static span(...intervals: F32Interval[]): F32Interval {
+ assert(intervals.length > 0, `span of an empty list of F32Intervals is not allowed`);
+ let begin = Number.POSITIVE_INFINITY;
+ let end = Number.NEGATIVE_INFINITY;
+ intervals.forEach(i => {
+ begin = Math.min(i.begin, begin);
+ end = Math.max(i.end, end);
+ });
+ return new F32Interval(begin, end);
+ }
+
+ /** @returns a string representation for logging purposes */
+ public toString(): string {
+ return `[${this.bounds()}]`;
+ }
+
+ /** @returns a singleton for interval of all possible values
+ * This interval is used in situations where accuracy is not defined, so any
+ * result is valid.
+ */
+ public static any(): F32Interval {
+ if (this._any === undefined) {
+ this._any = new F32Interval(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY);
+ }
+ return this._any;
+ }
+}
+
+/**
+ * SerializedF32Interval holds the serialized form of a F32Interval.
+ * This form can be safely encoded to JSON.
+ */
+export type SerializedF32Interval = { begin: number; end: number } | 'any';
+
+/** serializeF32Interval() converts a F32Interval to a SerializedF32Interval */
+export function serializeF32Interval(i: F32Interval): SerializedF32Interval {
+ return i === F32Interval.any()
+ ? 'any'
+ : { begin: reinterpretF32AsU32(i.begin), end: reinterpretF32AsU32(i.end) };
+}
+
+/** serializeF32Interval() converts a SerializedF32Interval to a F32Interval */
+export function deserializeF32Interval(data: SerializedF32Interval): F32Interval {
+ return data === 'any'
+ ? F32Interval.any()
+ : toF32Interval([reinterpretU32AsF32(data.begin), reinterpretU32AsF32(data.end)]);
+}
+
+/** @returns an interval containing the point or the original interval */
+export function toF32Interval(n: number | IntervalBounds | F32Interval): F32Interval {
+ if (n instanceof F32Interval) {
+ return n;
+ }
+
+ if (n instanceof Array) {
+ return new F32Interval(...n);
+ }
+
+ return new F32Interval(n, n);
+}
+
+/** F32Interval of [-π, π] */
+const kNegPiToPiInterval = toF32Interval([
+ kValue.f32.negative.pi.whole,
+ kValue.f32.positive.pi.whole,
+]);
+
+/** F32Interval of values greater than 0 and less than or equal to f32 max */
+const kGreaterThanZeroInterval = toF32Interval([
+ kValue.f32.subnormal.positive.min,
+ kValue.f32.positive.max,
+]);
+
+/** Representation of a vec2/3/4 of floating point intervals as an array of F32Intervals */
+export type F32Vector =
+ | [F32Interval, F32Interval]
+ | [F32Interval, F32Interval, F32Interval]
+ | [F32Interval, F32Interval, F32Interval, F32Interval];
+
+/** Coerce F32Interval[] to F32Vector if possible */
+function isF32Vector(v: number[] | IntervalBounds[] | F32Interval[] | F32Vector): v is F32Vector {
+ if (v[0] instanceof F32Interval) {
+ return v.length === 2 || v.length === 3 || v.length === 4;
+ }
+ return false;
+}
+
+/** @returns an F32Vector representation of an array fo F32Intervals if possible */
+export function toF32Vector(v: number[] | IntervalBounds[] | F32Interval[] | F32Vector): F32Vector {
+ if (isF32Vector(v)) {
+ return v;
+ }
+
+ const f = v.map(toF32Interval);
+ if (isF32Vector(f)) {
+ return f;
+ }
+ unreachable(`Cannot convert [${v}] to F32Vector`);
+}
+
+/** F32Vector with all zero elements */
+const kZeroVector = {
+ 2: toF32Vector([0, 0]),
+ 3: toF32Vector([0, 0, 0]),
+ 4: toF32Vector([0, 0, 0, 0]),
+};
+
+/** F32Vector with all F32Interval.any() elements */
+const kAnyVector = {
+ 2: toF32Vector([F32Interval.any(), F32Interval.any()]),
+ 3: toF32Vector([F32Interval.any(), F32Interval.any(), F32Interval.any()]),
+ 4: toF32Vector([F32Interval.any(), F32Interval.any(), F32Interval.any(), F32Interval.any()]),
+};
+
+/**
+ * @returns a F32Vector where each element is the span for corresponding
+ * elements at the same index in the input vectors
+ */
+function spanF32Vector(...vectors: F32Vector[]): F32Vector {
+ const vector_length = vectors[0].length;
+ assert(
+ vectors.every(e => e.length === vector_length),
+ `Vector span is not defined for vectors of differing lengths`
+ );
+
+ // The outer map is doing the walk across a single F32Vector to get the indices to use.
+ // The inner map is doing the walk across the of the vector array, collecting the value of each vector at the
+ // index, then spanning them down to a single F32Interval.
+ // The toF32Vector coerces things at the end to be a F32Vector, because the outer .map() will actually return a
+ // F32Interval[]
+ return toF32Vector(
+ vectors[0].map((_, idx) => {
+ return F32Interval.span(...vectors.map(v => v[idx]));
+ })
+ );
+}
+
+/**
+ * @retuns the vector result of multiplying the given vector by the given scalar
+ */
+function multiplyVectorByScalar(v: number[], c: number | F32Interval): F32Vector {
+ return toF32Vector(v.map(x => multiplicationInterval(x, c)));
+}
+
+/**
+ * @returns the input plus zero if any of the entries are f32 subnormal,
+ * otherwise returns the input.
+ */
+function addFlushedIfNeededF32(values: number[]): number[] {
+ return values.some(v => v !== 0 && isSubnormalNumberF32(v)) ? values.concat(0) : values;
+}
+
+/**
+ * @returns the input plus zero if any of the entries are f16 subnormal,
+ * otherwise returns the input
+ */
+function addFlushedIfNeededF16(values: number[]): number[] {
+ return values.some(v => v !== 0 && isSubnormalNumberF16(v)) ? values.concat(0) : values;
+}
+
+/**
+ * A function that converts a point to an acceptance interval.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface PointToInterval {
+ (x: number): F32Interval;
+}
+
+/** Operation used to implement a PointToInterval */
+export interface PointToIntervalOp {
+ /** @returns acceptance interval for a function at point x */
+ impl: PointToInterval;
+
+ /**
+ * Calculates where in the domain defined by x the min/max extrema of impl
+ * occur and returns a span of those points to be used as the domain instead.
+ *
+ * Used by runPointToIntervalOp before invoking impl.
+ * If not defined, the bounds of the existing domain are assumed to be the
+ * extrema.
+ *
+ * This is only implemented for operations that meet all of the following
+ * criteria:
+ * a) non-monotonic
+ * b) used in inherited accuracy calculations
+ * c) need to take in an interval for b)
+ * i.e. fooInterval takes in x: number | F32Interval, not x: number
+ */
+ extrema?: (x: F32Interval) => F32Interval;
+}
+
+/**
+ * Restrict the inputs to an PointToInterval operation
+ *
+ * Only used for operations that have tighter domain requirements than 'must be
+ * f32 finite'.
+ *
+ * @param domain interval to restrict inputs to
+ * @param impl operation implementation to run if input is within the required domain
+ * @returns a PointToInterval that calls impl if domain contains the input,
+ * otherwise it returns the any() interval */
+function limitPointToIntervalDomain(domain: F32Interval, impl: PointToInterval): PointToInterval {
+ return (n: number): F32Interval => {
+ return domain.contains(n) ? impl(n) : F32Interval.any();
+ };
+}
+
+/**
+ * A function that converts a pair of points to an acceptance interval.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface BinaryToInterval {
+ (x: number, y: number): F32Interval;
+}
+
+/** Operation used to implement a BinaryToInterval */
+interface BinaryToIntervalOp {
+ /** @returns acceptance interval for a function at point (x, y) */
+ impl: BinaryToInterval;
+ /**
+ * Calculates where in domain defined by x & y the min/max extrema of impl
+ * occur and returns spans of those points to be used as the domain instead.
+ *
+ * Used by runBinaryToIntervalOp before invoking impl.
+ * If not defined, the bounds of the existing domain are assumed to be the
+ * extrema.
+ *
+ * This is only implemented for functions that meet all of the following
+ * criteria:
+ * a) non-monotonic
+ * b) used in inherited accuracy calculations
+ * c) need to take in an interval for b)
+ */
+ extrema?: (x: F32Interval, y: F32Interval) => [F32Interval, F32Interval];
+}
+
+/** Domain for a BinaryToInterval implementation */
+interface BinaryToIntervalDomain {
+ // Arrays to support discrete valid domain intervals
+ x: F32Interval[];
+ y: F32Interval[];
+}
+
+/**
+ * Restrict the inputs to a BinaryToInterval
+ *
+ * Only used for operations that have tighter domain requirements than 'must be
+ * f32 finite'.
+ *
+ * @param domain set of intervals to restrict inputs to
+ * @param impl operation implementation to run if input is within the required domain
+ * @returns a BinaryToInterval that calls impl if domain contains the input,
+ * otherwise it returns the any() interval */
+function limitBinaryToIntervalDomain(
+ domain: BinaryToIntervalDomain,
+ impl: BinaryToInterval
+): BinaryToInterval {
+ return (x: number, y: number): F32Interval => {
+ if (!domain.x.some(d => d.contains(x)) || !domain.y.some(d => d.contains(y))) {
+ return F32Interval.any();
+ }
+
+ return impl(x, y);
+ };
+}
+
+/**
+ * A function that converts a triplet of points to an acceptance interval.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface TernaryToInterval {
+ (x: number, y: number, z: number): F32Interval;
+}
+
+/** Operation used to implement a TernaryToInterval */
+interface TernaryToIntervalOp {
+ // Re-using the *Op interface pattern for symmetry with the other operations.
+ /** @returns acceptance interval for a function at point (x, y, z) */
+ impl: TernaryToInterval;
+}
+
+// Currently PointToVector is not integrated with the rest of the floating point
+// framework, because the only builtins that use it are actually
+// u32 -> [f32, f32, f32, f32] functions, so the whole rounding and interval
+// process doesn't get applied to the inputs.
+// They do use the framework internally by invoking divisionInterval on segments
+// of the input.
+/**
+ * A function that converts a point to a vector of acceptance intervals.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface PointToVector {
+ (n: number): F32Vector;
+}
+
+/**
+ * A function that converts a vector to an acceptance interval.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface VectorToInterval {
+ (x: number[]): F32Interval;
+}
+
+/** Operation used to implement a VectorToInterval */
+interface VectorToIntervalOp {
+ // Re-using the *Op interface pattern for symmetry with the other operations.
+ /** @returns acceptance interval for a function on vector x */
+ impl: VectorToInterval;
+}
+
+/**
+ * A function that converts a pair of vectors to an acceptance interval.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface VectorPairToInterval {
+ (x: number[], y: number[]): F32Interval;
+}
+
+/** Operation used to implement a VectorPairToInterval */
+interface VectorPairToIntervalOp {
+ // Re-using the *Op interface pattern for symmetry with the other operations.
+ /** @returns acceptance interval for a function on vectors (x, y) */
+ impl: VectorPairToInterval;
+}
+
+/**
+ * A function that converts a vector to a vector of acceptance intervals.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface VectorToVector {
+ (x: number[]): F32Vector;
+}
+
+/** Operation used to implement a VectorToVector */
+interface VectorToVectorOp {
+ // Re-using the *Op interface pattern for symmetry with the other operations.
+ /** @returns a vector of acceptance intervals for a function on vector x */
+ impl: VectorToVector;
+}
+
+/**
+ * A function that converts a pair of vectors to a vector of acceptance
+ * intervals.
+ * This is the public facing API for builtin implementations that is called
+ * from tests.
+ */
+export interface VectorPairToVector {
+ (x: number[], y: number[]): F32Vector;
+}
+
+/** Operation used to implement a VectorPairToVector */
+interface VectorPairToVectorOp {
+ // Re-using the *Op interface pattern for symmetry with the other operations.
+ /** @returns a vector of acceptance intervals for a function on vectors (x, y) */
+ impl: VectorPairToVector;
+}
+
+/** Converts a point to an acceptance interval, using a specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ * op.extrema is invoked before this point in the call stack.
+ * op.domain is tested before this point in the call stack.
+ *
+ * @param n value to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function roundAndFlushPointToInterval(n: number, op: PointToIntervalOp) {
+ assert(!Number.isNaN(n), `flush not defined for NaN`);
+ const values = correctlyRoundedF32(n);
+ const inputs = addFlushedIfNeededF32(values);
+ const results = new Set<F32Interval>(inputs.map(op.impl));
+ return F32Interval.span(...results);
+}
+
+/** Converts a pair to an acceptance interval, using a specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ * All unique combinations of x & y are run.
+ * op.extrema is invoked before this point in the call stack.
+ * op.domain is tested before this point in the call stack.
+ *
+ * @param x first param to flush & round then invoke op.impl on
+ * @param y second param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function roundAndFlushBinaryToInterval(x: number, y: number, op: BinaryToIntervalOp): F32Interval {
+ assert(!Number.isNaN(x), `flush not defined for NaN`);
+ assert(!Number.isNaN(y), `flush not defined for NaN`);
+ const x_values = correctlyRoundedF32(x);
+ const y_values = correctlyRoundedF32(y);
+ const x_inputs = addFlushedIfNeededF32(x_values);
+ const y_inputs = addFlushedIfNeededF32(y_values);
+ const intervals = new Set<F32Interval>();
+ x_inputs.forEach(inner_x => {
+ y_inputs.forEach(inner_y => {
+ intervals.add(op.impl(inner_x, inner_y));
+ });
+ });
+ return F32Interval.span(...intervals);
+}
+
+/** Converts a triplet to an acceptance interval, using a specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ * All unique combinations of x, y & z are run.
+ *
+ * @param x first param to flush & round then invoke op.impl on
+ * @param y second param to flush & round then invoke op.impl on
+ * @param z third param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function roundAndFlushTernaryToInterval(
+ x: number,
+ y: number,
+ z: number,
+ op: TernaryToIntervalOp
+): F32Interval {
+ assert(!Number.isNaN(x), `flush not defined for NaN`);
+ assert(!Number.isNaN(y), `flush not defined for NaN`);
+ assert(!Number.isNaN(z), `flush not defined for NaN`);
+ const x_values = correctlyRoundedF32(x);
+ const y_values = correctlyRoundedF32(y);
+ const z_values = correctlyRoundedF32(z);
+ const x_inputs = addFlushedIfNeededF32(x_values);
+ const y_inputs = addFlushedIfNeededF32(y_values);
+ const z_inputs = addFlushedIfNeededF32(z_values);
+ const intervals = new Set<F32Interval>();
+ // prettier-ignore
+ x_inputs.forEach(inner_x => {
+ y_inputs.forEach(inner_y => {
+ z_inputs.forEach(inner_z => {
+ intervals.add(op.impl(inner_x, inner_y, inner_z));
+ });
+ });
+ });
+
+ return F32Interval.span(...intervals);
+}
+
+/** Converts a vector to an acceptance interval using a specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ *
+ * @param x param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function roundAndFlushVectorToInterval(x: number[], op: VectorToIntervalOp): F32Interval {
+ assert(
+ x.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+
+ const x_rounded: number[][] = x.map(correctlyRoundedF32);
+ const x_flushed: number[][] = x_rounded.map(addFlushedIfNeededF32);
+ const x_inputs = cartesianProduct<number>(...x_flushed);
+
+ const intervals = new Set<F32Interval>();
+ x_inputs.forEach(inner_x => {
+ intervals.add(op.impl(inner_x));
+ });
+ return F32Interval.span(...intervals);
+}
+
+/** Converts a pair of vectors to an acceptance interval using a specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ * All unique combinations of x & y are run.
+ *
+ * @param x first param to flush & round then invoke op.impl on
+ * @param y second param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function roundAndFlushVectorPairToInterval(
+ x: number[],
+ y: number[],
+ op: VectorPairToIntervalOp
+): F32Interval {
+ assert(
+ x.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+ assert(
+ y.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+
+ const x_rounded: number[][] = x.map(correctlyRoundedF32);
+ const y_rounded: number[][] = y.map(correctlyRoundedF32);
+ const x_flushed: number[][] = x_rounded.map(addFlushedIfNeededF32);
+ const y_flushed: number[][] = y_rounded.map(addFlushedIfNeededF32);
+ const x_inputs = cartesianProduct<number>(...x_flushed);
+ const y_inputs = cartesianProduct<number>(...y_flushed);
+
+ const intervals = new Set<F32Interval>();
+ x_inputs.forEach(inner_x => {
+ y_inputs.forEach(inner_y => {
+ intervals.add(op.impl(inner_x, inner_y));
+ });
+ });
+ return F32Interval.span(...intervals);
+}
+
+/** Converts a vector to a vector of acceptance intervals using a specific
+ * function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ *
+ * @param x param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a vector of spans for each outputs of op.impl
+ */
+function roundAndFlushVectorToVector(x: number[], op: VectorToVectorOp): F32Vector {
+ assert(
+ x.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+
+ const x_rounded: number[][] = x.map(correctlyRoundedF32);
+ const x_flushed: number[][] = x_rounded.map(addFlushedIfNeededF32);
+ const x_inputs = cartesianProduct<number>(...x_flushed);
+
+ const interval_vectors = new Set<F32Vector>();
+ x_inputs.forEach(inner_x => {
+ interval_vectors.add(op.impl(inner_x));
+ });
+
+ return spanF32Vector(...interval_vectors);
+}
+
+/**
+ * Converts a pair of vectors to a vector of acceptance intervals using a
+ * specific function
+ *
+ * This handles correctly rounding and flushing inputs as needed.
+ * Duplicate inputs are pruned before invoking op.impl.
+ *
+ * @param x first param to flush & round then invoke op.impl on
+ * @param x second param to flush & round then invoke op.impl on
+ * @param op operation defining the function being run
+ * @returns a vector of spans for each output of op.impl
+ */
+function roundAndFlushVectorPairToVector(
+ x: number[],
+ y: number[],
+ op: VectorPairToVectorOp
+): F32Vector {
+ assert(
+ x.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+ assert(
+ y.every(e => !Number.isNaN(e)),
+ `flush not defined for NaN`
+ );
+
+ const x_rounded: number[][] = x.map(correctlyRoundedF32);
+ const y_rounded: number[][] = y.map(correctlyRoundedF32);
+ const x_flushed: number[][] = x_rounded.map(addFlushedIfNeededF32);
+ const y_flushed: number[][] = y_rounded.map(addFlushedIfNeededF32);
+ const x_inputs = cartesianProduct<number>(...x_flushed);
+ const y_inputs = cartesianProduct<number>(...y_flushed);
+
+ const interval_vectors = new Set<F32Vector>();
+ x_inputs.forEach(inner_x => {
+ y_inputs.forEach(inner_y => {
+ interval_vectors.add(op.impl(inner_x, inner_y));
+ });
+ });
+
+ return spanF32Vector(...interval_vectors);
+}
+
+/** Calculate the acceptance interval for a unary function over an interval
+ *
+ * If the interval is actually a point, this just decays to
+ * roundAndFlushPointToInterval.
+ *
+ * The provided domain interval may be adjusted if the operation defines an
+ * extrema function.
+ *
+ * @param x input domain interval
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function runPointToIntervalOp(x: F32Interval, op: PointToIntervalOp): F32Interval {
+ if (!x.isFinite()) {
+ return F32Interval.any();
+ }
+
+ if (op.extrema !== undefined) {
+ x = op.extrema(x);
+ }
+
+ const result = F32Interval.span(...x.bounds().map(b => roundAndFlushPointToInterval(b, op)));
+ return result.isFinite() ? result : F32Interval.any();
+}
+
+/** Calculate the acceptance interval for a binary function over an interval
+ *
+ * The provided domain intervals may be adjusted if the operation defines an
+ * extrema function.
+ *
+ * @param x first input domain interval
+ * @param y second input domain interval
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function runBinaryToIntervalOp(
+ x: F32Interval,
+ y: F32Interval,
+ op: BinaryToIntervalOp
+): F32Interval {
+ if (!x.isFinite() || !y.isFinite()) {
+ return F32Interval.any();
+ }
+
+ if (op.extrema !== undefined) {
+ [x, y] = op.extrema(x, y);
+ }
+
+ const outputs = new Set<F32Interval>();
+ x.bounds().forEach(inner_x => {
+ y.bounds().forEach(inner_y => {
+ outputs.add(roundAndFlushBinaryToInterval(inner_x, inner_y, op));
+ });
+ });
+
+ const result = F32Interval.span(...outputs);
+ return result.isFinite() ? result : F32Interval.any();
+}
+
+/** Calculate the acceptance interval for a ternary function over an interval
+ *
+ * @param x first input domain interval
+ * @param y second input domain interval
+ * @param z third input domain interval
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function runTernaryToIntervalOp(
+ x: F32Interval,
+ y: F32Interval,
+ z: F32Interval,
+ op: TernaryToIntervalOp
+): F32Interval {
+ if (!x.isFinite() || !y.isFinite() || !z.isFinite()) {
+ return F32Interval.any();
+ }
+
+ const outputs = new Set<F32Interval>();
+ x.bounds().forEach(inner_x => {
+ y.bounds().forEach(inner_y => {
+ z.bounds().forEach(inner_z => {
+ outputs.add(roundAndFlushTernaryToInterval(inner_x, inner_y, inner_z, op));
+ });
+ });
+ });
+
+ const result = F32Interval.span(...outputs);
+ return result.isFinite() ? result : F32Interval.any();
+}
+
+/** Calculate the acceptance interval for a vector function over given intervals
+ *
+ * @param x input domain intervals vector
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function runVectorToIntervalOp(x: F32Vector, op: VectorToIntervalOp): F32Interval {
+ if (x.some(e => !e.isFinite())) {
+ return F32Interval.any();
+ }
+
+ const x_values = cartesianProduct<number>(...x.map(e => e.bounds()));
+
+ const outputs = new Set<F32Interval>();
+ x_values.forEach(inner_x => {
+ outputs.add(roundAndFlushVectorToInterval(inner_x, op));
+ });
+
+ const result = F32Interval.span(...outputs);
+ return result.isFinite() ? result : F32Interval.any();
+}
+
+/** Calculate the acceptance interval for a vector pair function over given intervals
+ *
+ * @param x first input domain intervals vector
+ * @param y second input domain intervals vector
+ * @param op operation defining the function being run
+ * @returns a span over all of the outputs of op.impl
+ */
+function runVectorPairToIntervalOp(
+ x: F32Vector,
+ y: F32Vector,
+ op: VectorPairToIntervalOp
+): F32Interval {
+ if (x.some(e => !e.isFinite()) || y.some(e => !e.isFinite())) {
+ return F32Interval.any();
+ }
+
+ const x_values = cartesianProduct<number>(...x.map(e => e.bounds()));
+ const y_values = cartesianProduct<number>(...y.map(e => e.bounds()));
+
+ const outputs = new Set<F32Interval>();
+ x_values.forEach(inner_x => {
+ y_values.forEach(inner_y => {
+ outputs.add(roundAndFlushVectorPairToInterval(inner_x, inner_y, op));
+ });
+ });
+
+ const result = F32Interval.span(...outputs);
+ return result.isFinite() ? result : F32Interval.any();
+}
+
+/** Calculate the vector of acceptance intervals for a pair of vector function over
+ * given intervals
+ *
+ * @param x input domain intervals vector
+ * @param x input domain intervals vector
+ * @param op operation defining the function being run
+ * @returns a vector of spans over all of the outputs of op.impl
+ */
+function runVectorToVectorOp(x: F32Vector, op: VectorToVectorOp): F32Vector {
+ if (x.some(e => !e.isFinite())) {
+ return kAnyVector[x.length];
+ }
+
+ const x_values = cartesianProduct<number>(...x.map(e => e.bounds()));
+
+ const outputs = new Set<F32Vector>();
+ x_values.forEach(inner_x => {
+ outputs.add(roundAndFlushVectorToVector(inner_x, op));
+ });
+
+ const result = spanF32Vector(...outputs);
+ return result.every(e => e.isFinite()) ? result : toF32Vector(x.map(_ => F32Interval.any()));
+}
+
+/**
+ * Calculate the vector of acceptance intervals by running a scalar operation
+ * component-wise over a vector.
+ *
+ * This is used for situations where a component-wise operation, like vector
+ * negation, is needed as part of a inherited accuracy, but the top-level
+ * operation test don't require an explicit vector definition of the function,
+ * due to the generated vectorize tests being sufficient.
+ *
+ * @param x input domain intervals vector
+ * @param op scalar operation to be run component-wise
+ * @returns a vector of intervals with the outputs of op.impl
+ */
+function runPointToIntervalOpComponentWise(x: F32Vector, op: PointToIntervalOp): F32Vector {
+ return toF32Vector(
+ x.map(i => {
+ return runPointToIntervalOp(i, op);
+ })
+ );
+}
+
+/** Calculate the vector of acceptance intervals for a vector function over
+ * given intervals
+ *
+ * @param x first input domain intervals vector
+ * @param y second input domain intervals vector
+ * @param op operation defining the function being run
+ * @returns a vector of spans over all of the outputs of op.impl
+ */
+function runVectorPairToVectorOp(x: F32Vector, y: F32Vector, op: VectorPairToVectorOp): F32Vector {
+ if (x.some(e => !e.isFinite()) || y.some(e => !e.isFinite())) {
+ return kAnyVector[x.length];
+ }
+
+ const x_values = cartesianProduct<number>(...x.map(e => e.bounds()));
+ const y_values = cartesianProduct<number>(...y.map(e => e.bounds()));
+
+ const outputs = new Set<F32Vector>();
+ x_values.forEach(inner_x => {
+ y_values.forEach(inner_y => {
+ outputs.add(roundAndFlushVectorPairToVector(inner_x, inner_y, op));
+ });
+ });
+
+ const result = spanF32Vector(...outputs);
+ return result.every(e => e.isFinite()) ? result : toF32Vector(x.map(_ => F32Interval.any()));
+}
+
+/**
+ * Calculate the vector of acceptance intervals by running a scalar operation
+ * component-wise over a pair vectors.
+ *
+ * This is used for situations where a component-wise operation, like vector
+ * subtraction, is needed as part of a inherited accuracy, but the top-level
+ * operation test don't require an explicit vector definition of the function,
+ * due to the generated vectorize tests being sufficient.
+ *
+ * @param x first input domain intervals vector
+ * @param y second input domain intervals vector
+ * @param op scalar operation to be run component-wise
+ * @returns a vector of intervals with the outputs of op.impl
+ */
+function runBinaryToIntervalOpComponentWise(
+ x: F32Vector,
+ y: F32Vector,
+ op: BinaryToIntervalOp
+): F32Vector {
+ assert(
+ x.length === y.length,
+ `runBinaryToIntervalOpComponentWise requires vectors of the same length`
+ );
+ return toF32Vector(
+ x.map((i, idx) => {
+ return runBinaryToIntervalOp(i, y[idx], op);
+ })
+ );
+}
+
+/** Defines a PointToIntervalOp for an interval of the correctly rounded values around the point */
+const CorrectlyRoundedIntervalOp: PointToIntervalOp = {
+ impl: (n: number) => {
+ assert(!Number.isNaN(n), `absolute not defined for NaN`);
+ return toF32Interval(n);
+ },
+};
+
+/** @returns an interval of the correctly rounded values around the point */
+export function correctlyRoundedInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), CorrectlyRoundedIntervalOp);
+}
+
+/** @returns a PointToIntervalOp for [n - error_range, n + error_range] */
+function AbsoluteErrorIntervalOp(error_range: number): PointToIntervalOp {
+ const op: PointToIntervalOp = {
+ impl: (_: number) => {
+ return F32Interval.any();
+ },
+ };
+
+ if (isFiniteF32(error_range)) {
+ op.impl = (n: number) => {
+ assert(!Number.isNaN(n), `absolute error not defined for NaN`);
+ return toF32Interval([n - error_range, n + error_range]);
+ };
+ }
+
+ return op;
+}
+
+/** @returns an interval of the absolute error around the point */
+export function absoluteErrorInterval(n: number, error_range: number): F32Interval {
+ error_range = Math.abs(error_range);
+ return runPointToIntervalOp(toF32Interval(n), AbsoluteErrorIntervalOp(error_range));
+}
+
+/** @returns a PointToIntervalOp for [n - numULP * ULP(n), n + numULP * ULP(n)] */
+function ULPIntervalOp(numULP: number): PointToIntervalOp {
+ const op: PointToIntervalOp = {
+ impl: (_: number) => {
+ return F32Interval.any();
+ },
+ };
+
+ if (isFiniteF32(numULP)) {
+ op.impl = (n: number) => {
+ assert(!Number.isNaN(n), `ULP error not defined for NaN`);
+
+ const ulp = oneULP(n);
+ const begin = n - numULP * ulp;
+ const end = n + numULP * ulp;
+
+ return toF32Interval([
+ Math.min(begin, flushSubnormalNumberF32(begin)),
+ Math.max(end, flushSubnormalNumberF32(end)),
+ ]);
+ };
+ }
+
+ return op;
+}
+
+/** @returns an interval of N * ULP around the point */
+export function ulpInterval(n: number, numULP: number): F32Interval {
+ numULP = Math.abs(numULP);
+ return runPointToIntervalOp(toF32Interval(n), ULPIntervalOp(numULP));
+}
+
+const AbsIntervalOp: PointToIntervalOp = {
+ impl: (n: number) => {
+ return correctlyRoundedInterval(Math.abs(n));
+ },
+};
+
+/** Calculate an acceptance interval for abs(n) */
+export function absInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AbsIntervalOp);
+}
+
+const AcosIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(toF32Interval([-1.0, 1.0]), (n: number) => {
+ // acos(n) = atan2(sqrt(1.0 - n * n), n)
+ const y = sqrtInterval(subtractionInterval(1, multiplicationInterval(n, n)));
+ return atan2Interval(y, n);
+ }),
+};
+
+/** Calculate an acceptance interval for acos(n) */
+export function acosInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AcosIntervalOp);
+}
+
+/** All acceptance interval functions for acosh(x) */
+export const acoshIntervals: PointToInterval[] = [acoshAlternativeInterval, acoshPrimaryInterval];
+
+const AcoshAlternativeIntervalOp: PointToIntervalOp = {
+ impl: (x: number): F32Interval => {
+ // acosh(x) = log(x + sqrt((x + 1.0f) * (x - 1.0)))
+ const inner_value = multiplicationInterval(
+ additionInterval(x, 1.0),
+ subtractionInterval(x, 1.0)
+ );
+ const sqrt_value = sqrtInterval(inner_value);
+ return logInterval(additionInterval(x, sqrt_value));
+ },
+};
+
+/** Calculate an acceptance interval of acosh(x) using log(x + sqrt((x + 1.0f) * (x - 1.0))) */
+export function acoshAlternativeInterval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), AcoshAlternativeIntervalOp);
+}
+
+const AcoshPrimaryIntervalOp: PointToIntervalOp = {
+ impl: (x: number): F32Interval => {
+ // acosh(x) = log(x + sqrt(x * x - 1.0))
+ const inner_value = subtractionInterval(multiplicationInterval(x, x), 1.0);
+ const sqrt_value = sqrtInterval(inner_value);
+ return logInterval(additionInterval(x, sqrt_value));
+ },
+};
+
+/** Calculate an acceptance interval of acosh(x) using log(x + sqrt(x * x - 1.0)) */
+export function acoshPrimaryInterval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), AcoshPrimaryIntervalOp);
+}
+
+const AdditionIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return correctlyRoundedInterval(x + y);
+ },
+};
+
+/** Calculate an acceptance interval of x + y */
+export function additionInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), AdditionIntervalOp);
+}
+
+const AsinIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(toF32Interval([-1.0, 1.0]), (n: number) => {
+ // asin(n) = atan2(n, sqrt(1.0 - n * n))
+ const x = sqrtInterval(subtractionInterval(1, multiplicationInterval(n, n)));
+ return atan2Interval(n, x);
+ }),
+};
+
+/** Calculate an acceptance interval for asin(n) */
+export function asinInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AsinIntervalOp);
+}
+
+const AsinhIntervalOp: PointToIntervalOp = {
+ impl: (x: number): F32Interval => {
+ // asinh(x) = log(x + sqrt(x * x + 1.0))
+ const inner_value = additionInterval(multiplicationInterval(x, x), 1.0);
+ const sqrt_value = sqrtInterval(inner_value);
+ return logInterval(additionInterval(x, sqrt_value));
+ },
+};
+
+/** Calculate an acceptance interval of asinh(x) */
+export function asinhInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AsinhIntervalOp);
+}
+
+const AtanIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return ulpInterval(Math.atan(n), 4096);
+ },
+};
+
+/** Calculate an acceptance interval of atan(x) */
+export function atanInterval(n: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AtanIntervalOp);
+}
+
+const Atan2IntervalOp: BinaryToIntervalOp = {
+ impl: limitBinaryToIntervalDomain(
+ {
+ // For atan2, there params are labelled (y, x), not (x, y), so domain.x is first parameter (y), and domain.y is
+ // the second parameter (x)
+ x: [
+ toF32Interval([kValue.f32.negative.min, kValue.f32.negative.max]),
+ toF32Interval([kValue.f32.positive.min, kValue.f32.positive.max]),
+ ], // first param must be finite and normal
+ y: [toF32Interval([-(2 ** 126), -(2 ** -126)]), toF32Interval([2 ** -126, 2 ** 126])], // inherited from division
+ },
+ (y: number, x: number): F32Interval => {
+ const atan_yx = Math.atan(y / x);
+ // x > 0, atan(y/x)
+ if (x > 0) {
+ return ulpInterval(atan_yx, 4096);
+ }
+
+ // x < 0, y > 0, atan(y/x) + π
+ if (y > 0) {
+ return ulpInterval(atan_yx + kValue.f32.positive.pi.whole, 4096);
+ }
+
+ // x < 0, y < 0, atan(y/x) - π
+ return ulpInterval(atan_yx - kValue.f32.positive.pi.whole, 4096);
+ }
+ ),
+ extrema: (y: F32Interval, x: F32Interval): [F32Interval, F32Interval] => {
+ // There is discontinuity + undefined behaviour at y/x = 0 that will dominate the accuracy
+ if (y.contains(0)) {
+ if (x.contains(0)) {
+ return [toF32Interval(0), toF32Interval(0)];
+ }
+ return [toF32Interval(0), x];
+ }
+ return [y, x];
+ },
+};
+
+/** Calculate an acceptance interval of atan2(y, x) */
+export function atan2Interval(y: number | F32Interval, x: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(y), toF32Interval(x), Atan2IntervalOp);
+}
+
+const AtanhIntervalOp: PointToIntervalOp = {
+ impl: (n: number) => {
+ // atanh(x) = log((1.0 + x) / (1.0 - x)) * 0.5
+ const numerator = additionInterval(1.0, n);
+ const denominator = subtractionInterval(1.0, n);
+ const log_interval = logInterval(divisionInterval(numerator, denominator));
+ return multiplicationInterval(log_interval, 0.5);
+ },
+};
+
+/** Calculate an acceptance interval of atanh(x) */
+export function atanhInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), AtanhIntervalOp);
+}
+
+const CeilIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return correctlyRoundedInterval(Math.ceil(n));
+ },
+};
+
+/** Calculate an acceptance interval of ceil(x) */
+export function ceilInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), CeilIntervalOp);
+}
+
+const ClampMedianIntervalOp: TernaryToIntervalOp = {
+ impl: (x: number, y: number, z: number): F32Interval => {
+ return correctlyRoundedInterval(
+ // Default sort is string sort, so have to implement numeric comparison.
+ // Cannot use the b-a one liner, because that assumes no infinities.
+ [x, y, z].sort((a, b) => {
+ if (a < b) {
+ return -1;
+ }
+ if (a > b) {
+ return 1;
+ }
+ return 0;
+ })[1]
+ );
+ },
+};
+
+/** All acceptance interval functions for clamp(x, y, z) */
+export const clampIntervals: TernaryToInterval[] = [clampMinMaxInterval, clampMedianInterval];
+
+/** Calculate an acceptance interval of clamp(x, y, z) via median(x, y, z) */
+export function clampMedianInterval(
+ x: number | F32Interval,
+ y: number | F32Interval,
+ z: number | F32Interval
+): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(x),
+ toF32Interval(y),
+ toF32Interval(z),
+ ClampMedianIntervalOp
+ );
+}
+
+const ClampMinMaxIntervalOp: TernaryToIntervalOp = {
+ impl: (x: number, low: number, high: number): F32Interval => {
+ return correctlyRoundedInterval(Math.min(Math.max(x, low), high));
+ },
+};
+
+/** Calculate an acceptance interval of clamp(x, high, low) via min(max(x, low), high) */
+export function clampMinMaxInterval(
+ x: number | F32Interval,
+ low: number | F32Interval,
+ high: number | F32Interval
+): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(x),
+ toF32Interval(low),
+ toF32Interval(high),
+ ClampMinMaxIntervalOp
+ );
+}
+
+const CosIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(
+ kNegPiToPiInterval,
+ (n: number): F32Interval => {
+ return absoluteErrorInterval(Math.cos(n), 2 ** -11);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of cos(x) */
+export function cosInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), CosIntervalOp);
+}
+
+const CoshIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ // cosh(x) = (exp(x) + exp(-x)) * 0.5
+ const minus_n = negationInterval(n);
+ return multiplicationInterval(additionInterval(expInterval(n), expInterval(minus_n)), 0.5);
+ },
+};
+
+/** Calculate an acceptance interval of cosh(x) */
+export function coshInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), CoshIntervalOp);
+}
+
+const CrossIntervalOp: VectorPairToVectorOp = {
+ impl: (x: number[], y: number[]): F32Vector => {
+ assert(x.length === 3, `CrossIntervalOp received x with ${x.length} instead of 3`);
+ assert(y.length === 3, `CrossIntervalOp received y with ${y.length} instead of 3`);
+
+ // cross(x, y) = r, where
+ // r[0] = x[1] * y[2] - x[2] * y[1]
+ // r[1] = x[2] * y[0] - x[0] * y[2]
+ // r[2] = x[0] * y[1] - x[1] * y[0]
+
+ const r0 = subtractionInterval(
+ multiplicationInterval(x[1], y[2]),
+ multiplicationInterval(x[2], y[1])
+ );
+ const r1 = subtractionInterval(
+ multiplicationInterval(x[2], y[0]),
+ multiplicationInterval(x[0], y[2])
+ );
+ const r2 = subtractionInterval(
+ multiplicationInterval(x[0], y[1]),
+ multiplicationInterval(x[1], y[0])
+ );
+ return [r0, r1, r2];
+ },
+};
+
+export function crossInterval(x: number[], y: number[]): F32Vector {
+ assert(x.length === 3, `Cross is only defined for vec3`);
+ assert(y.length === 3, `Cross is only defined for vec3`);
+ return runVectorPairToVectorOp(toF32Vector(x), toF32Vector(y), CrossIntervalOp);
+}
+
+const DegreesIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return multiplicationInterval(n, 57.295779513082322865);
+ },
+};
+
+/** Calculate an acceptance interval of degrees(x) */
+export function degreesInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), DegreesIntervalOp);
+}
+
+const DistanceIntervalScalarOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return lengthInterval(subtractionInterval(x, y));
+ },
+};
+
+const DistanceIntervalVectorOp: VectorPairToIntervalOp = {
+ impl: (x: number[], y: number[]): F32Interval => {
+ return lengthInterval(
+ runBinaryToIntervalOpComponentWise(toF32Vector(x), toF32Vector(y), SubtractionIntervalOp)
+ );
+ },
+};
+
+/** Calculate an acceptance interval of distance(x, y) */
+export function distanceInterval(x: number | number[], y: number | number[]): F32Interval {
+ if (x instanceof Array && y instanceof Array) {
+ assert(
+ x.length === y.length,
+ `distanceInterval requires both params to have the same number of elements`
+ );
+ return runVectorPairToIntervalOp(toF32Vector(x), toF32Vector(y), DistanceIntervalVectorOp);
+ } else if (!(x instanceof Array) && !(y instanceof Array)) {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), DistanceIntervalScalarOp);
+ }
+ unreachable(
+ `distanceInterval requires both params to both the same type, either scalars or vectors`
+ );
+}
+
+const DivisionIntervalOp: BinaryToIntervalOp = {
+ impl: limitBinaryToIntervalDomain(
+ {
+ x: [toF32Interval([kValue.f32.negative.min, kValue.f32.positive.max])],
+ y: [toF32Interval([-(2 ** 126), -(2 ** -126)]), toF32Interval([2 ** -126, 2 ** 126])],
+ },
+ (x: number, y: number): F32Interval => {
+ if (y === 0) {
+ return F32Interval.any();
+ }
+ return ulpInterval(x / y, 2.5);
+ }
+ ),
+ extrema: (x: F32Interval, y: F32Interval): [F32Interval, F32Interval] => {
+ // division has a discontinuity at y = 0.
+ if (y.contains(0)) {
+ y = toF32Interval(0);
+ }
+ return [x, y];
+ },
+};
+
+/** Calculate an acceptance interval of x / y */
+export function divisionInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), DivisionIntervalOp);
+}
+
+const DotIntervalOp: VectorPairToIntervalOp = {
+ impl: (x: number[], y: number[]): F32Interval => {
+ // dot(x, y) = sum of x[i] * y[i]
+ const multiplications = runBinaryToIntervalOpComponentWise(
+ toF32Vector(x),
+ toF32Vector(y),
+ MultiplicationIntervalOp
+ );
+
+ // vec2 doesn't require permutations, since a + b = b + a for floats
+ if (multiplications.length === 2) {
+ return additionInterval(multiplications[0], multiplications[1]);
+ }
+
+ // The spec does not state the ordering of summation, so all of the permutations are calculated and their results
+ // spanned, since addition of more then two floats is not transitive, i.e. a + b + c is not guaranteed to equal
+ // b + a + c
+ const permutations: F32Interval[][] = calculatePermutations(multiplications);
+ return F32Interval.span(
+ ...permutations.map(p => p.reduce((prev, cur) => additionInterval(prev, cur)))
+ );
+ },
+};
+
+export function dotInterval(x: number[], y: number[]): F32Interval {
+ assert(x.length === y.length, `dot not defined for vectors with different lengths`);
+ return runVectorPairToIntervalOp(toF32Vector(x), toF32Vector(y), DotIntervalOp);
+}
+
+const ExpIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return ulpInterval(Math.exp(n), 3 + 2 * Math.abs(n));
+ },
+};
+
+/** Calculate an acceptance interval for exp(x) */
+export function expInterval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), ExpIntervalOp);
+}
+
+const Exp2IntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return ulpInterval(Math.pow(2, n), 3 + 2 * Math.abs(n));
+ },
+};
+
+/** Calculate an acceptance interval for exp2(x) */
+export function exp2Interval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), Exp2IntervalOp);
+}
+
+/**
+ * Calculate the acceptance intervals for faceForward(x, y, z)
+ *
+ * faceForward(x, y, z) = select(-x, x, dot(z, y) < 0.0)
+ *
+ * This builtin selects from two discrete results (delta rounding/flushing), so
+ * the majority of the framework code is not appropriate, since the framework
+ * attempts to span results.
+ *
+ * Thus a bespoke implementation is used instead of
+ * defining a Op and running that through the framework.
+ */
+export function faceForwardIntervals(
+ x: number[],
+ y: number[],
+ z: number[]
+): (F32Vector | undefined)[] {
+ const x_vec = toF32Vector(x);
+ // Running vector through runPointToIntervalOpComponentWise to make sure that flushing/rounding is handled, since
+ // toF32Vector does not perform those operations.
+ const positive_x = runPointToIntervalOpComponentWise(x_vec, { impl: toF32Interval });
+ const negative_x = runPointToIntervalOpComponentWise(x_vec, NegationIntervalOp);
+
+ const dot_interval = dotInterval(z, y);
+
+ const results: (F32Vector | undefined)[] = [];
+
+ if (!dot_interval.isFinite()) {
+ // dot calculation went out of bounds
+ // Inserting undefine in the result, so that the test running framework is aware
+ // of this potential OOB.
+ // For const-eval tests, it means that the test case should be skipped,
+ // since the shader will fail to compile.
+ // For non-const-eval the undefined should be stripped out of the possible
+ // results.
+
+ results.push(undefined);
+ }
+
+ // Because the result of dot can be an interval, it might span across 0, thus
+ // it is possible that both -x and x are valid responses.
+ if (dot_interval.begin < 0 || dot_interval.end < 0) {
+ results.push(positive_x);
+ }
+
+ if (dot_interval.begin >= 0 || dot_interval.end >= 0) {
+ results.push(negative_x);
+ }
+
+ assert(
+ results.length > 0 || results.every(r => r === undefined),
+ `faceForwardInterval selected neither positive x or negative x for the result, this shouldn't be possible`
+ );
+ return results;
+}
+
+const FloorIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return correctlyRoundedInterval(Math.floor(n));
+ },
+};
+
+/** Calculate an acceptance interval of floor(x) */
+export function floorInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), FloorIntervalOp);
+}
+
+const FmaIntervalOp: TernaryToIntervalOp = {
+ impl: (x: number, y: number, z: number): F32Interval => {
+ return additionInterval(multiplicationInterval(x, y), z);
+ },
+};
+
+/** Calculate an acceptance interval for fma(x, y, z) */
+export function fmaInterval(x: number, y: number, z: number): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(x),
+ toF32Interval(y),
+ toF32Interval(z),
+ FmaIntervalOp
+ );
+}
+
+const FractIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ // fract(x) = x - floor(x) is defined in the spec.
+ // For people coming from a non-graphics background this will cause some unintuitive results. For example,
+ // fract(-1.1) is not 0.1 or -0.1, but instead 0.9.
+ // This is how other shading languages operate and allows for a desirable wrap around in graphics programming.
+ const result = subtractionInterval(n, floorInterval(n));
+ if (result.contains(1)) {
+ // Very small negative numbers can lead to catastrophic cancellation, thus calculating a fract of 1.0, which is
+ // technically not a fractional part, so some implementations clamp the result to next nearest number.
+ return F32Interval.span(result, toF32Interval(kValue.f32.positive.less_than_one));
+ }
+ return result;
+ },
+};
+
+/** Calculate an acceptance interval of fract(x) */
+export function fractInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), FractIntervalOp);
+}
+
+const InverseSqrtIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(
+ kGreaterThanZeroInterval,
+ (n: number): F32Interval => {
+ return ulpInterval(1 / Math.sqrt(n), 2);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of inverseSqrt(x) */
+export function inverseSqrtInterval(n: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), InverseSqrtIntervalOp);
+}
+
+const LdexpIntervalOp: BinaryToIntervalOp = {
+ impl: limitBinaryToIntervalDomain(
+ // Implementing SPIR-V's more restrictive domain until
+ // https://github.com/gpuweb/gpuweb/issues/3134 is resolved
+ {
+ x: [toF32Interval([kValue.f32.negative.min, kValue.f32.positive.max])],
+ y: [toF32Interval([-126, 128])],
+ },
+ (e1: number, e2: number): F32Interval => {
+ // Though the spec says the result of ldexp(e1, e2) = e1 * 2 ^ e2, the
+ // accuracy is listed as correctly rounded to the true value, so the
+ // inheritance framework does not need to be invoked to determine bounds.
+ // Instead the value at a higher precision is calculated and passed to
+ // correctlyRoundedInterval.
+ const result = e1 * 2 ** e2;
+ if (Number.isNaN(result)) {
+ // Overflowed TS's number type, so definitely out of bounds for f32
+ return F32Interval.any();
+ }
+ return correctlyRoundedInterval(result);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of ldexp(e1, e2) */
+export function ldexpInterval(e1: number, e2: number): F32Interval {
+ return roundAndFlushBinaryToInterval(e1, e2, LdexpIntervalOp);
+}
+
+const LengthIntervalScalarOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return sqrtInterval(multiplicationInterval(n, n));
+ },
+};
+
+const LengthIntervalVectorOp: VectorToIntervalOp = {
+ impl: (n: number[]): F32Interval => {
+ return sqrtInterval(dotInterval(n, n));
+ },
+};
+
+/** Calculate an acceptance interval of length(x) */
+export function lengthInterval(n: number | F32Interval | number[] | F32Vector): F32Interval {
+ if (n instanceof Array) {
+ return runVectorToIntervalOp(toF32Vector(n), LengthIntervalVectorOp);
+ } else {
+ return runPointToIntervalOp(toF32Interval(n), LengthIntervalScalarOp);
+ }
+}
+
+const LogIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(
+ kGreaterThanZeroInterval,
+ (n: number): F32Interval => {
+ if (n >= 0.5 && n <= 2.0) {
+ return absoluteErrorInterval(Math.log(n), 2 ** -21);
+ }
+ return ulpInterval(Math.log(n), 3);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of log(x) */
+export function logInterval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), LogIntervalOp);
+}
+
+const Log2IntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(
+ kGreaterThanZeroInterval,
+ (n: number): F32Interval => {
+ if (n >= 0.5 && n <= 2.0) {
+ return absoluteErrorInterval(Math.log2(n), 2 ** -21);
+ }
+ return ulpInterval(Math.log2(n), 3);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of log2(x) */
+export function log2Interval(x: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(x), Log2IntervalOp);
+}
+
+const MaxIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return correctlyRoundedInterval(Math.max(x, y));
+ },
+};
+
+/** Calculate an acceptance interval of max(x, y) */
+export function maxInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), MaxIntervalOp);
+}
+
+const MinIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return correctlyRoundedInterval(Math.min(x, y));
+ },
+};
+
+/** Calculate an acceptance interval of min(x, y) */
+export function minInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), MinIntervalOp);
+}
+
+const MixImpreciseIntervalOp: TernaryToIntervalOp = {
+ impl: (x: number, y: number, z: number): F32Interval => {
+ // x + (y - x) * z =
+ // x + t, where t = (y - x) * z
+ const t = multiplicationInterval(subtractionInterval(y, x), z);
+ return additionInterval(x, t);
+ },
+};
+
+/** All acceptance interval functions for mix(x, y, z) */
+export const mixIntervals: TernaryToInterval[] = [mixImpreciseInterval, mixPreciseInterval];
+
+/** Calculate an acceptance interval of mix(x, y, z) using x + (y - x) * z */
+export function mixImpreciseInterval(x: number, y: number, z: number): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(x),
+ toF32Interval(y),
+ toF32Interval(z),
+ MixImpreciseIntervalOp
+ );
+}
+
+const MixPreciseIntervalOp: TernaryToIntervalOp = {
+ impl: (x: number, y: number, z: number): F32Interval => {
+ // x * (1.0 - z) + y * z =
+ // t + s, where t = x * (1.0 - z), s = y * z
+ const t = multiplicationInterval(x, subtractionInterval(1.0, z));
+ const s = multiplicationInterval(y, z);
+ return additionInterval(t, s);
+ },
+};
+
+/** Calculate an acceptance interval of mix(x, y, z) using x * (1.0 - z) + y * z */
+export function mixPreciseInterval(x: number, y: number, z: number): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(x),
+ toF32Interval(y),
+ toF32Interval(z),
+ MixPreciseIntervalOp
+ );
+}
+
+/** Calculate an acceptance interval of modf(x) */
+export function modfInterval(n: number): { fract: F32Interval; whole: F32Interval } {
+ const fract = correctlyRoundedInterval(n % 1.0);
+ const whole = correctlyRoundedInterval(n - (n % 1.0));
+ return { fract, whole };
+}
+
+const MultiplicationInnerOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return correctlyRoundedInterval(x * y);
+ },
+};
+
+const MultiplicationIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return roundAndFlushBinaryToInterval(x, y, MultiplicationInnerOp);
+ },
+};
+
+/** Calculate an acceptance interval of x * y */
+export function multiplicationInterval(
+ x: number | F32Interval,
+ y: number | F32Interval
+): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), MultiplicationIntervalOp);
+}
+
+const NegationIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return correctlyRoundedInterval(-n);
+ },
+};
+
+/** Calculate an acceptance interval of -x */
+export function negationInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), NegationIntervalOp);
+}
+
+const NormalizeIntervalOp: VectorToVectorOp = {
+ impl: (n: number[]): F32Vector => {
+ const length = lengthInterval(n);
+ return toF32Vector(n.map(e => divisionInterval(e, length)));
+ },
+};
+
+/** Calculate an acceptance interval of normalize(x) */
+export function normalizeInterval(n: number[]): F32Vector {
+ return runVectorToVectorOp(toF32Vector(n), NormalizeIntervalOp);
+}
+
+const PowIntervalOp: BinaryToIntervalOp = {
+ // pow(x, y) has no explicit domain restrictions, but inherits the x <= 0
+ // domain restriction from log2(x). Invoking log2Interval(x) in impl will
+ // enforce this, so there is no need to wrap the impl call here.
+ impl: (x: number, y: number): F32Interval => {
+ return exp2Interval(multiplicationInterval(y, log2Interval(x)));
+ },
+};
+
+/** Calculate an acceptance interval of pow(x, y) */
+export function powInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), PowIntervalOp);
+}
+
+// Once a full implementation of F16Interval exists, the correctlyRounded for
+// that can potentially be used instead of having a bespoke operation
+// implementation.
+const QuantizeToF16IntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ const rounded = correctlyRoundedF16(n);
+ const flushed = addFlushedIfNeededF16(rounded);
+ return F32Interval.span(...flushed.map(toF32Interval));
+ },
+};
+
+/** Calculate an acceptance interval of quanitizeToF16(x) */
+export function quantizeToF16Interval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), QuantizeToF16IntervalOp);
+}
+
+const RadiansIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return multiplicationInterval(n, 0.017453292519943295474);
+ },
+};
+
+/** Calculate an acceptance interval of radians(x) */
+export function radiansInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), RadiansIntervalOp);
+}
+
+const ReflectIntervalOp: VectorPairToVectorOp = {
+ impl: (x: number[], y: number[]): F32Vector => {
+ assert(
+ x.length === y.length,
+ `ReflectIntervalOp received x (${x}) and y (${y}) with different numbers of elements`
+ );
+
+ // reflect(x, y) = x - 2.0 * dot(x, y) * y
+ // = x - t * y, t = 2.0 * dot(x, y)
+ // x = incident vector
+ // y = normal of reflecting surface
+ const t = multiplicationInterval(2.0, dotInterval(x, y));
+ const rhs = multiplyVectorByScalar(y, t);
+ return runBinaryToIntervalOpComponentWise(toF32Vector(x), rhs, SubtractionIntervalOp);
+ },
+};
+
+/** Calculate an acceptance interval of reflect(x, y) */
+export function reflectInterval(x: number[], y: number[]): F32Vector {
+ assert(
+ x.length === y.length,
+ `reflect is only defined for vectors with the same number of elements`
+ );
+ return runVectorPairToVectorOp(toF32Vector(x), toF32Vector(y), ReflectIntervalOp);
+}
+
+/**
+ * Calculate acceptance interval vectors of reflect(i, s, r)
+ *
+ * refract is a singular function in the sense that it is the only builtin that
+ * takes in (F32Vector, F32Vector, F32) and returns F32Vector and is basically
+ * defined in terms of other functions.
+ *
+ * Instead of implementing all of the framework code to integrate it with its
+ * own operation type/etc, it instead has a bespoke implementation that is a
+ * composition of other builtin functions that use the framework.
+ */
+export function refractInterval(i: number[], s: number[], r: number): F32Vector {
+ assert(
+ i.length === s.length,
+ `refract is only defined for vectors with the same number of elements`
+ );
+
+ const r_squared = multiplicationInterval(r, r);
+ const dot = dotInterval(s, i);
+ const dot_squared = multiplicationInterval(dot, dot);
+ const one_minus_dot_squared = subtractionInterval(1, dot_squared);
+ const k = subtractionInterval(1.0, multiplicationInterval(r_squared, one_minus_dot_squared));
+
+ if (!k.isFinite() || k.containsZeroOrSubnormals()) {
+ // There is a discontinuity at k == 0, due to sqrt(k) being calculated, so exiting early
+ return kAnyVector[toF32Vector(i).length];
+ }
+
+ if (k.end < 0.0) {
+ // if k is negative, then the zero vector is the valid response
+ return kZeroVector[toF32Vector(i).length];
+ }
+
+ const dot_times_r = multiplicationInterval(dot, r);
+ const k_sqrt = sqrtInterval(k);
+ const t = additionInterval(dot_times_r, k_sqrt); // t = r * dot(i, s) + sqrt(k)
+
+ const result = runBinaryToIntervalOpComponentWise(
+ multiplyVectorByScalar(i, r),
+ multiplyVectorByScalar(s, t),
+ SubtractionIntervalOp
+ ); // (i * r) - (s * t)
+ return result;
+}
+
+const RemainderIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ // x % y = x - y * trunc(x/y)
+ return subtractionInterval(x, multiplicationInterval(y, truncInterval(divisionInterval(x, y))));
+ },
+};
+
+/** Calculate an acceptance interval for x % y */
+export function remainderInterval(x: number, y: number): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), RemainderIntervalOp);
+}
+
+const RoundIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ const k = Math.floor(n);
+ const diff_before = n - k;
+ const diff_after = k + 1 - n;
+ if (diff_before < diff_after) {
+ return correctlyRoundedInterval(k);
+ } else if (diff_before > diff_after) {
+ return correctlyRoundedInterval(k + 1);
+ }
+
+ // n is in the middle of two integers.
+ // The tie breaking rule is 'k if k is even, k + 1 if k is odd'
+ if (k % 2 === 0) {
+ return correctlyRoundedInterval(k);
+ }
+ return correctlyRoundedInterval(k + 1);
+ },
+};
+
+/** Calculate an acceptance interval of round(x) */
+export function roundInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), RoundIntervalOp);
+}
+
+/**
+ * Calculate an acceptance interval of saturate(n) as clamp(n, 0.0, 1.0)
+ *
+ * The definition of saturate is such that both possible implementations of
+ * clamp will return the same value, so arbitrarily picking the minmax version
+ * to use.
+ */
+export function saturateInterval(n: number): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(n),
+ toF32Interval(0.0),
+ toF32Interval(1.0),
+ ClampMinMaxIntervalOp
+ );
+}
+
+const SignIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ if (n > 0.0) {
+ return correctlyRoundedInterval(1.0);
+ }
+ if (n < 0.0) {
+ return correctlyRoundedInterval(-1.0);
+ }
+
+ return correctlyRoundedInterval(0.0);
+ },
+};
+
+/** Calculate an acceptance interval of sin(x) */
+export function signInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), SignIntervalOp);
+}
+
+const SinIntervalOp: PointToIntervalOp = {
+ impl: limitPointToIntervalDomain(
+ kNegPiToPiInterval,
+ (n: number): F32Interval => {
+ return absoluteErrorInterval(Math.sin(n), 2 ** -11);
+ }
+ ),
+};
+
+/** Calculate an acceptance interval of sin(x) */
+export function sinInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), SinIntervalOp);
+}
+
+const SinhIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ // sinh(x) = (exp(x) - exp(-x)) * 0.5
+ const minus_n = negationInterval(n);
+ return multiplicationInterval(subtractionInterval(expInterval(n), expInterval(minus_n)), 0.5);
+ },
+};
+
+/** Calculate an acceptance interval of sinh(x) */
+export function sinhInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), SinhIntervalOp);
+}
+
+const SmoothStepOp: TernaryToIntervalOp = {
+ impl: (low: number, high: number, x: number): F32Interval => {
+ // For clamp(foo, 0.0, 1.0) the different implementations of clamp provide
+ // the same value, so arbitrarily picking the minmax version to use.
+ // t = clamp((x - low) / (high - low), 0.0, 1.0)
+ // prettier-ignore
+ const t = clampMedianInterval(
+ divisionInterval(
+ subtractionInterval(x, low),
+ subtractionInterval(high, low)),
+ 0.0,
+ 1.0);
+ // Inherited from t * t * (3.0 - 2.0 * t)
+ // prettier-ignore
+ return multiplicationInterval(
+ t,
+ multiplicationInterval(t,
+ subtractionInterval(3.0,
+ multiplicationInterval(2.0, t))));
+ },
+};
+
+/** Calculate an acceptance interval of smoothStep(low, high, x) */
+export function smoothStepInterval(low: number, high: number, x: number): F32Interval {
+ return runTernaryToIntervalOp(
+ toF32Interval(low),
+ toF32Interval(high),
+ toF32Interval(x),
+ SmoothStepOp
+ );
+}
+
+const SqrtIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return divisionInterval(1.0, inverseSqrtInterval(n));
+ },
+};
+
+/** Calculate an acceptance interval of sqrt(x) */
+export function sqrtInterval(n: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), SqrtIntervalOp);
+}
+
+const StepIntervalOp: BinaryToIntervalOp = {
+ impl: (edge: number, x: number): F32Interval => {
+ if (edge <= x) {
+ return correctlyRoundedInterval(1.0);
+ }
+ return correctlyRoundedInterval(0.0);
+ },
+};
+
+/** Calculate an acceptance 'interval' for step(edge, x)
+ *
+ * step only returns two possible values, so its interval requires special
+ * interpretation in CTS tests.
+ * This interval will be one of four values: [0, 0], [0, 1], [1, 1] & [-∞, +∞].
+ * [0, 0] and [1, 1] indicate that the correct answer in point they encapsulate.
+ * [0, 1] should not be treated as a span, i.e. 0.1 is acceptable, but instead
+ * indicate either 0.0 or 1.0 are acceptable answers.
+ * [-∞, +∞] is treated as the any interval, since an undefined or infinite value was passed in.
+ */
+export function stepInterval(edge: number, x: number): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(edge), toF32Interval(x), StepIntervalOp);
+}
+
+const SubtractionIntervalOp: BinaryToIntervalOp = {
+ impl: (x: number, y: number): F32Interval => {
+ return correctlyRoundedInterval(x - y);
+ },
+};
+
+/** Calculate an acceptance interval of x - y */
+export function subtractionInterval(x: number | F32Interval, y: number | F32Interval): F32Interval {
+ return runBinaryToIntervalOp(toF32Interval(x), toF32Interval(y), SubtractionIntervalOp);
+}
+
+const TanIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return divisionInterval(sinInterval(n), cosInterval(n));
+ },
+};
+
+/** Calculate an acceptance interval of tan(x) */
+export function tanInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), TanIntervalOp);
+}
+
+const TanhIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return divisionInterval(sinhInterval(n), coshInterval(n));
+ },
+};
+
+/** Calculate an acceptance interval of tanh(x) */
+export function tanhInterval(n: number): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), TanhIntervalOp);
+}
+
+const TruncIntervalOp: PointToIntervalOp = {
+ impl: (n: number): F32Interval => {
+ return correctlyRoundedInterval(Math.trunc(n));
+ },
+};
+
+/** Calculate an acceptance interval of trunc(x) */
+export function truncInterval(n: number | F32Interval): F32Interval {
+ return runPointToIntervalOp(toF32Interval(n), TruncIntervalOp);
+}
+
+/**
+ * Once-allocated ArrayBuffer/views to avoid overhead of allocation when converting between numeric formats
+ *
+ * unpackData* is shared between all of the unpack*Interval functions, so to avoid re-entrancy problems, they should
+ * not call each other or themselves directly or indirectly.
+ */
+const unpackData = new ArrayBuffer(4);
+const unpackDataU32 = new Uint32Array(unpackData);
+const unpackDataU16 = new Uint16Array(unpackData);
+const unpackDataU8 = new Uint8Array(unpackData);
+const unpackDataI16 = new Int16Array(unpackData);
+const unpackDataI8 = new Int8Array(unpackData);
+const unpackDataF16 = new Float16Array(unpackData);
+
+/** Calculate an acceptance interval vector for unpack2x16float(x) */
+export function unpack2x16floatInterval(n: number): F32Vector {
+ assert(
+ n >= kValue.u32.min && n <= kValue.u32.max,
+ 'unpack2x16floatInterval only accepts values on the bounds of u32'
+ );
+ unpackDataU32[0] = n;
+ if (unpackDataF16.some(f => !isFiniteF16(f))) {
+ return [F32Interval.any(), F32Interval.any()];
+ }
+
+ const result: F32Vector = [
+ quantizeToF16Interval(unpackDataF16[0]),
+ quantizeToF16Interval(unpackDataF16[1]),
+ ];
+
+ if (result.some(r => !r.isFinite())) {
+ return [F32Interval.any(), F32Interval.any()];
+ }
+ return result;
+}
+
+const Unpack2x16snormIntervalOp = (n: number): F32Interval => {
+ return maxInterval(divisionInterval(n, 32767), -1);
+};
+
+/** Calculate an acceptance interval vector for unpack2x16snorm(x) */
+export function unpack2x16snormInterval(n: number): F32Vector {
+ assert(
+ n >= kValue.u32.min && n <= kValue.u32.max,
+ 'unpack2x16snormInterval only accepts values on the bounds of u32'
+ );
+ unpackDataU32[0] = n;
+ return [Unpack2x16snormIntervalOp(unpackDataI16[0]), Unpack2x16snormIntervalOp(unpackDataI16[1])];
+}
+
+const Unpack2x16unormIntervalOp = (n: number): F32Interval => {
+ return divisionInterval(n, 65535);
+};
+
+/** Calculate an acceptance interval vector for unpack2x16unorm(x) */
+export function unpack2x16unormInterval(n: number): F32Vector {
+ assert(
+ n >= kValue.u32.min && n <= kValue.u32.max,
+ 'unpack2x16unormInterval only accepts values on the bounds of u32'
+ );
+ unpackDataU32[0] = n;
+ return [Unpack2x16unormIntervalOp(unpackDataU16[0]), Unpack2x16unormIntervalOp(unpackDataU16[1])];
+}
+
+const Unpack4x8snormIntervalOp = (n: number): F32Interval => {
+ return maxInterval(divisionInterval(n, 127), -1);
+};
+
+/** Calculate an acceptance interval vector for unpack4x8snorm(x) */
+export function unpack4x8snormInterval(n: number): F32Vector {
+ assert(
+ n >= kValue.u32.min && n <= kValue.u32.max,
+ 'unpack4x8snormInterval only accepts values on the bounds of u32'
+ );
+ unpackDataU32[0] = n;
+ return [
+ Unpack4x8snormIntervalOp(unpackDataI8[0]),
+ Unpack4x8snormIntervalOp(unpackDataI8[1]),
+ Unpack4x8snormIntervalOp(unpackDataI8[2]),
+ Unpack4x8snormIntervalOp(unpackDataI8[3]),
+ ];
+}
+
+const Unpack4x8unormIntervalOp = (n: number): F32Interval => {
+ return divisionInterval(n, 255);
+};
+
+/** Calculate an acceptance interval vector for unpack4x8unorm(x) */
+export function unpack4x8unormInterval(n: number): F32Vector {
+ assert(
+ n >= kValue.u32.min && n <= kValue.u32.max,
+ 'unpack4x8unormInterval only accepts values on the bounds of u32'
+ );
+ unpackDataU32[0] = n;
+ return [
+ Unpack4x8unormIntervalOp(unpackDataU8[0]),
+ Unpack4x8unormIntervalOp(unpackDataU8[1]),
+ Unpack4x8unormIntervalOp(unpackDataU8[2]),
+ Unpack4x8unormIntervalOp(unpackDataU8[3]),
+ ];
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts
new file mode 100644
index 0000000000..07779a8de1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/math.ts
@@ -0,0 +1,962 @@
+import { assert } from '../../common/util/util.js';
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+
+import { kBit, kValue } from './constants.js';
+import {
+ f16,
+ f16Bits,
+ f32,
+ f32Bits,
+ floatBitsToNumber,
+ i32,
+ kFloat16Format,
+ kFloat32Format,
+ Scalar,
+ u32,
+} from './conversion.js';
+
+/**
+ * A multiple of 8 guaranteed to be way too large to allocate (just under 8 pebibytes).
+ * This is a "safe" integer (ULP <= 1.0) very close to MAX_SAFE_INTEGER.
+ *
+ * Note: allocations of this size are likely to exceed limitations other than just the system's
+ * physical memory, so test cases are also needed to try to trigger "true" OOM.
+ */
+export const kMaxSafeMultipleOf8 = Number.MAX_SAFE_INTEGER - 7;
+
+/** Round `n` up to the next multiple of `alignment` (inclusive). */
+// MAINTENANCE_TODO: Rename to `roundUp`
+export function align(n: number, alignment: number): number {
+ assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
+ assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
+ return Math.ceil(n / alignment) * alignment;
+}
+
+/** Round `n` down to the next multiple of `alignment` (inclusive). */
+export function roundDown(n: number, alignment: number): number {
+ assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
+ assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
+ return Math.floor(n / alignment) * alignment;
+}
+
+/** Clamp a number to the provided range. */
+export function clamp(n: number, { min, max }: { min: number; max: number }): number {
+ assert(max >= min);
+ return Math.min(Math.max(n, min), max);
+}
+
+/** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */
+export function flushSubnormalNumberF32(val: number): number {
+ return isSubnormalNumberF32(val) ? 0 : val;
+}
+
+/** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */
+export function flushSubnormalScalarF32(val: Scalar): Scalar {
+ return isSubnormalScalarF32(val) ? f32(0) : val;
+}
+
+/**
+ * @returns true if |val| is a subnormal f32 number, otherwise returns false
+ * 0 is considered a non-subnormal number by this function.
+ */
+export function isSubnormalScalarF32(val: Scalar): boolean {
+ if (val.type.kind !== 'f32') {
+ return false;
+ }
+
+ if (val === f32(0)) {
+ return false;
+ }
+
+ const u32_val = new Uint32Array(new Float32Array([val.value.valueOf() as number]).buffer)[0];
+ return (u32_val & 0x7f800000) === 0;
+}
+
+/** U/** @returns if number is within subnormal range of f32 */
+export function isSubnormalNumberF32(n: number): boolean {
+ return n > kValue.f32.negative.max && n < kValue.f32.positive.min;
+}
+
+/** @returns if number is in the finite range of f32 */
+export function isFiniteF32(n: number) {
+ return n >= kValue.f32.negative.min && n <= kValue.f32.positive.max;
+}
+
+/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */
+export function flushSubnormalNumberF16(val: number): number {
+ return isSubnormalNumberF16(val) ? 0 : val;
+}
+
+/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */
+export function flushSubnormalScalarF16(val: Scalar): Scalar {
+ return isSubnormalScalarF16(val) ? f16(0) : val;
+}
+
+/**
+ * @returns true if |val| is a subnormal f16 number, otherwise returns false
+ * 0 is considered a non-subnormal number by this function.
+ */
+export function isSubnormalScalarF16(val: Scalar): boolean {
+ if (val.type.kind !== 'f16') {
+ return false;
+ }
+
+ if (val === f16(0)) {
+ return false;
+ }
+
+ const u16_val = new Uint16Array(new Float16Array([val.value.valueOf() as number]).buffer)[0];
+ return (u16_val & 0x7f800000) === 0;
+}
+
+/** @returns if number is within subnormal range of f16 */
+export function isSubnormalNumberF16(n: number): boolean {
+ return n > kValue.f16.negative.max && n < kValue.f16.positive.min;
+}
+
+/** @returns if number is in the finite range of f16 */
+export function isFiniteF16(n: number) {
+ return n >= kValue.f16.negative.min && n <= kValue.f16.positive.max;
+}
+
+/** Should FTZ occur during calculations or not */
+export type FlushMode = 'flush' | 'no-flush';
+
+/**
+ * @returns the next f32 value after |val|,
+ * towards +inf if |dir| is true, otherwise towards -inf.
+
+ * If |mpode| is 'flush', all subnormal values will be flushed to 0,
+ * before processing and for -/+0 the nextAfterF32 will be the closest normal in
+ * the correct direction.
+
+ * If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
+ * and for -/+0 the nextAfterF32 will be the closest subnormal in the correct
+ * direction.
+ *
+ * val needs to be in [min f32, max f32]
+ */
+export function nextAfterF32(val: number, dir: boolean = true, mode: FlushMode): Scalar {
+ if (Number.isNaN(val)) {
+ return f32Bits(kBit.f32.nan.positive.s);
+ }
+
+ if (val === Number.POSITIVE_INFINITY) {
+ return f32Bits(kBit.f32.infinity.positive);
+ }
+
+ if (val === Number.NEGATIVE_INFINITY) {
+ return f32Bits(kBit.f32.infinity.negative);
+ }
+
+ assert(
+ val <= kValue.f32.positive.max && val >= kValue.f32.negative.min,
+ `${val} is not in the range of float32`
+ );
+
+ val = mode === 'flush' ? flushSubnormalNumberF32(val) : val;
+
+ // -/+0 === 0 returns true
+ if (val === 0) {
+ if (dir) {
+ return mode === 'flush'
+ ? f32Bits(kBit.f32.positive.min)
+ : f32Bits(kBit.f32.subnormal.positive.min);
+ } else {
+ return mode === 'flush'
+ ? f32Bits(kBit.f32.negative.max)
+ : f32Bits(kBit.f32.subnormal.negative.max);
+ }
+ }
+
+ const converted: number = new Float32Array([val])[0];
+ let u32_result: number;
+ if (val === converted) {
+ // val is expressible precisely as a float32
+ u32_result = new Uint32Array(new Float32Array([val]).buffer)[0];
+ const is_positive = (u32_result & 0x80000000) === 0;
+ if (dir === is_positive) {
+ u32_result += 1;
+ } else {
+ u32_result -= 1;
+ }
+ } else {
+ // val had to be rounded to be expressed as a float32
+ if (dir === converted > val) {
+ // Rounding was in the direction requested
+ u32_result = new Uint32Array(new Float32Array([converted]).buffer)[0];
+ } else {
+ // Round was opposite of the direction requested, so need nextAfterF32 in the requested direction.
+ // This will not recurse since converted is guaranteed to be a float32 due to the conversion above.
+ const next = nextAfterF32(converted, dir, mode).value.valueOf() as number;
+ u32_result = new Uint32Array(new Float32Array([next]).buffer)[0];
+ }
+ }
+
+ // Checking for overflow
+ if ((u32_result & 0x7f800000) === 0x7f800000) {
+ if (dir) {
+ return f32Bits(kBit.f32.infinity.positive);
+ } else {
+ return f32Bits(kBit.f32.infinity.negative);
+ }
+ }
+
+ const f32_result = f32Bits(u32_result);
+ return mode === 'flush' ? flushSubnormalScalarF32(f32_result) : f32_result;
+}
+
+/**
+ * @returns the next f16 value after |val|,
+ * towards +inf if |dir| is true, otherwise towards -inf.
+ *
+ * If |mode| is true, all subnormal values will be flushed to 0,
+ * before processing, and for -/+0 the nextAfterF16 will be the closest normal
+ * in the correct direction
+ *
+ * If |mode| is false, the next subnormal will be calculated when appropriate,
+ * and for -/+0 the nextAfterF16 will be the closest subnormal in the correct
+ * direction.
+ *
+ * val needs to be in [min f16, max f16]
+ */
+export function nextAfterF16(val: number, dir: boolean = true, mode: FlushMode): Scalar {
+ if (Number.isNaN(val)) {
+ return f16Bits(kBit.f16.nan.positive.s);
+ }
+
+ if (val === Number.POSITIVE_INFINITY) {
+ return f16Bits(kBit.f16.infinity.positive);
+ }
+
+ if (val === Number.NEGATIVE_INFINITY) {
+ return f16Bits(kBit.f16.infinity.negative);
+ }
+
+ assert(
+ val <= kValue.f16.positive.max && val >= kValue.f16.negative.min,
+ `${val} is not in the range of float16`
+ );
+
+ val = mode === 'flush' ? flushSubnormalNumberF16(val) : val;
+
+ // -/+0 === 0 returns true
+ if (val === 0) {
+ if (dir) {
+ return mode === 'flush'
+ ? f16Bits(kBit.f16.positive.min)
+ : f16Bits(kBit.f16.subnormal.positive.min);
+ } else {
+ return mode === 'flush'
+ ? f16Bits(kBit.f16.negative.max)
+ : f16Bits(kBit.f16.subnormal.negative.max);
+ }
+ }
+
+ const converted: number = new Float16Array([val])[0];
+ let u16_result: number;
+ if (val === converted) {
+ // val is expressible precisely as a float16
+ u16_result = new Uint16Array(new Float16Array([val]).buffer)[0];
+ const is_positive = (u16_result & 0x8000) === 0;
+ if (dir === is_positive) {
+ u16_result += 1;
+ } else {
+ u16_result -= 1;
+ }
+ } else {
+ // val had to be rounded to be expressed as a float16
+ if (dir === converted > val) {
+ // Rounding was in the direction requested
+ u16_result = new Uint16Array(new Float16Array([converted]).buffer)[0];
+ } else {
+ // Round was opposite of the direction requested, so need nextAfterF16 in the requested direction.
+ // This will not recurse since converted is guaranteed to be a float16 due to the conversion above.
+ const next = nextAfterF16(converted, dir, mode).value.valueOf() as number;
+ u16_result = new Uint16Array(new Float16Array([next]).buffer)[0];
+ }
+ }
+
+ // Checking for overflow
+ if ((u16_result & 0x7f800000) === 0x7f800000) {
+ if (dir) {
+ return f16Bits(kBit.f16.infinity.positive);
+ } else {
+ return f16Bits(kBit.f16.infinity.negative);
+ }
+ }
+
+ const f16_result = f16Bits(u16_result);
+ return mode === 'flush' ? flushSubnormalScalarF16(f16_result) : f16_result;
+}
+
+/**
+ * @returns ulp(x), the unit of least precision for a specific number as a 32-bit float
+ *
+ * ulp(x) is the distance between the two floating point numbers nearest x.
+ * This value is also called unit of last place, ULP, and 1 ULP.
+ * See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
+ * for a more detailed/nuanced discussion of the definition of ulp(x).
+ *
+ * @param target number to calculate ULP for
+ * @param mode should FTZ occuring during calculation or not
+ */
+export function oneULP(target: number, mode: FlushMode = 'flush'): number {
+ if (Number.isNaN(target)) {
+ return Number.NaN;
+ }
+
+ target = mode === 'flush' ? flushSubnormalNumberF32(target) : target;
+
+ // For values at the edge of the range or beyond ulp(x) is defined as the distance between the two nearest
+ // f32 representable numbers to the appropriate edge.
+ if (target === Number.POSITIVE_INFINITY || target >= kValue.f32.positive.max) {
+ return kValue.f32.positive.max - kValue.f32.positive.nearest_max;
+ } else if (target === Number.NEGATIVE_INFINITY || target <= kValue.f32.negative.min) {
+ return kValue.f32.negative.nearest_min - kValue.f32.negative.min;
+ }
+
+ // ulp(x) is min(after - before), where
+ // before <= x <= after
+ // before =/= after
+ // before and after are f32 representable
+ const before = nextAfterF32(target, false, mode).value.valueOf() as number;
+ const after = nextAfterF32(target, true, mode).value.valueOf() as number;
+ const converted: number = new Float32Array([target])[0];
+ if (converted === target) {
+ // |target| is f32 representable, so either before or after will be x
+ return Math.min(target - before, after - target);
+ } else {
+ // |target| is not f32 representable so taking distance of neighbouring f32s.
+ return after - before;
+ }
+}
+
+/**
+ * Calculate the valid roundings when quantizing to 32-bit floats
+ *
+ * TS/JS's number type is internally a f64, so quantization needs to occur when
+ * converting to f32 for WGSL. WGSL does not specify a specific rounding mode,
+ * so if a number is not precisely representable in 32-bits, but in the
+ * range, there are two possible valid quantizations. If it is precisely
+ * representable, there is only one valid quantization. This function calculates
+ * the valid roundings and returns them in an array.
+ *
+ * This function does not consider flushing mode, so subnormals are maintained.
+ * The caller is responsible to flushing before and after as appropriate.
+ *
+ * Out of range values return the appropriate infinity and edge value.
+ *
+ * @param n number to be quantized
+ * @returns all of the acceptable roundings for quantizing to 32-bits in
+ * ascending order.
+ */
+export function correctlyRoundedF32(n: number): number[] {
+ assert(!Number.isNaN(n), `correctlyRoundedF32 not defined for NaN`);
+ // Above f32 range
+ if (n === Number.POSITIVE_INFINITY || n > kValue.f32.positive.max) {
+ return [kValue.f32.positive.max, Number.POSITIVE_INFINITY];
+ }
+
+ // Below f32 range
+ if (n === Number.NEGATIVE_INFINITY || n < kValue.f32.negative.min) {
+ return [Number.NEGATIVE_INFINITY, kValue.f32.negative.min];
+ }
+
+ const n_32 = new Float32Array([n])[0];
+ const converted: number = n_32;
+ if (n === converted) {
+ // n is precisely expressible as a f32, so should not be rounded
+ return [n];
+ }
+
+ if (converted > n) {
+ // n_32 rounded towards +inf, so is after n
+ const other = nextAfterF32(n_32, false, 'no-flush').value as number;
+ return [other, converted];
+ } else {
+ // n_32 rounded towards -inf, so is before n
+ const other = nextAfterF32(n_32, true, 'no-flush').value as number;
+ return [converted, other];
+ }
+}
+
+/**
+ * Calculate the valid roundings when quantizing to 16-bit floats
+ *
+ * TS/JS's number type is internally a f64, so quantization needs to occur when
+ * converting to f16 for WGSL. WGSL does not specify a specific rounding mode,
+ * so if a number is not precisely representable in 16-bits, but in the
+ * range, there are two possible valid quantizations. If it is precisely
+ * representable, there is only one valid quantization. This function calculates
+ * the valid roundings and returns them in an array.
+ *
+ * This function does not consider flushing mode, so subnormals are maintained.
+ * The caller is responsible to flushing before and after as appropriate.
+ *
+ * Out of range values return the appropriate infinity and edge value.
+ *
+ * @param n number to be quantized
+ * @returns all of the acceptable roundings for quantizing to 16-bits in
+ * ascending order.
+ */
+export function correctlyRoundedF16(n: number): number[] {
+ assert(!Number.isNaN(n), `correctlyRoundedF16 not defined for NaN`);
+ // Above f16 range
+ if (n === Number.POSITIVE_INFINITY || n > kValue.f16.positive.max) {
+ return [kValue.f16.positive.max, Number.POSITIVE_INFINITY];
+ }
+
+ // Below f16 range
+ if (n === Number.NEGATIVE_INFINITY || n < kValue.f16.negative.min) {
+ return [Number.NEGATIVE_INFINITY, kValue.f16.negative.min];
+ }
+
+ const n_16 = new Float16Array([n])[0];
+ const converted: number = n_16;
+ if (n === converted) {
+ // n is precisely expressible as a f16, so should not be rounded
+ return [n];
+ }
+
+ if (converted > n) {
+ // n_16 rounded towards +inf, so is after n
+ const other = nextAfterF16(n_16, false, 'no-flush').value as number;
+ return [other, converted];
+ } else {
+ // n_16 rounded towards -inf, so is before n
+ const other = nextAfterF16(n_16, true, 'no-flush').value as number;
+ return [converted, other];
+ }
+}
+
+/**
+ * Calculates the linear interpolation between two values of a given fractional.
+ *
+ * If |t| is 0, |a| is returned, if |t| is 1, |b| is returned, otherwise
+ * interpolation/extrapolation equivalent to a + t(b - a) is performed.
+ *
+ * Numerical stable version is adapted from http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0811r2.html
+ */
+export function lerp(a: number, b: number, t: number): number {
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
+ return Number.NaN;
+ }
+
+ if ((a <= 0.0 && b >= 0.0) || (a >= 0.0 && b <= 0.0)) {
+ return t * b + (1 - t) * a;
+ }
+
+ if (t === 1.0) {
+ return b;
+ }
+
+ const x = a + t * (b - a);
+ return t > 1.0 === b > a ? Math.max(b, x) : Math.min(b, x);
+}
+
+/** @returns a linear increasing range of numbers. */
+export function linearRange(a: number, b: number, num_steps: number): Array<number> {
+ if (num_steps <= 0) {
+ return Array<number>();
+ }
+
+ // Avoid division by 0
+ if (num_steps === 1) {
+ return [a];
+ }
+
+ return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, i / (num_steps - 1)));
+}
+
+/**
+ * @returns a non-linear increasing range of numbers, with a bias towards the beginning.
+ *
+ * Generates a linear range on [0,1] with |num_steps|, then squares all the values to make the curve be quadratic,
+ * thus biasing towards 0, but remaining on the [0, 1] range.
+ * This biased range is then scaled to the desired range using lerp.
+ * Different curves could be generated by changing c, where greater values of c will bias more towards 0.
+ */
+export function biasedRange(a: number, b: number, num_steps: number): Array<number> {
+ const c = 2;
+ if (num_steps <= 0) {
+ return Array<number>();
+ }
+
+ // Avoid division by 0
+ if (num_steps === 1) {
+ return [a];
+ }
+
+ return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, Math.pow(i / (num_steps - 1), c)));
+}
+
+/**
+ * @returns an ascending sorted array of numbers spread over the entire range of 32-bit floats
+ *
+ * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
+ * Zero is included.
+ *
+ * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
+ * means that number of precise f32 values between each returned value in a region should be about the same. This allows
+ * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f32 range.
+ *
+ * This function is intended to provide dense coverage of the f32 range, for a minimal list of values to use to cover
+ * f32 behaviour, use sparseF32Range instead.
+ *
+ * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
+ * must be 0 or greater.
+ */
+export function fullF32Range(
+ counts: {
+ neg_norm?: number;
+ neg_sub?: number;
+ pos_sub: number;
+ pos_norm: number;
+ } = { pos_sub: 10, pos_norm: 50 }
+): Array<number> {
+ counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
+ counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
+
+ // Generating bit fields first and then converting to f32, so that the spread across the possible f32 values is more
+ // even. Generating against the bounds of f32 values directly results in the values being extremely biased towards the
+ // extremes, since they are so much larger.
+ const bit_fields = [
+ ...linearRange(kBit.f32.negative.min, kBit.f32.negative.max, counts.neg_norm),
+ ...linearRange(
+ kBit.f32.subnormal.negative.min,
+ kBit.f32.subnormal.negative.max,
+ counts.neg_sub
+ ),
+ 0,
+ ...linearRange(
+ kBit.f32.subnormal.positive.min,
+ kBit.f32.subnormal.positive.max,
+ counts.pos_sub
+ ),
+ ...linearRange(kBit.f32.positive.min, kBit.f32.positive.max, counts.pos_norm),
+ ].map(Math.trunc);
+ return bit_fields.map(hexToF32);
+}
+
+/**
+ * @returns an ascending sorted array of numbers.
+ *
+ * The numbers returned are based on the `full32Range` as described above. The difference comes depending
+ * on the `source` parameter. If the `source` is `const` then the numbers will be restricted to be
+ * in the range `[low, high]`. This allows filtering out a set of `f32` values which are invalid for
+ * const-evaluation but are needed to test the non-const implementation.
+ *
+ * @param source the input source for the test. If the `source` is `const` then the return will be filtered
+ * @param low the lowest f32 value to permit when filtered
+ * @param high the highest f32 value to permit when filtered
+ */
+export function sourceFilteredF32Range(source: String, low: number, high: number): Array<number> {
+ return fullF32Range().filter(x => source !== 'const' || (x >= low && x <= high));
+}
+
+/**
+ * @returns an ascending sorted array of numbers spread over the entire range of 16-bit floats
+ *
+ * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
+ * Zero is included.
+ *
+ * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
+ * means that number of precise f16 values between each returned value in a region should be about the same. This allows
+ * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f16 range.
+ *
+ * This function is intended to provide dense coverage of the f16 range, for a minimal list of values to use to cover
+ * f16 behaviour, use sparseF16Range instead.
+ *
+ * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
+ * must be 0 or greater.
+ */
+export function fullF16Range(
+ counts: {
+ neg_norm?: number;
+ neg_sub?: number;
+ pos_sub: number;
+ pos_norm: number;
+ } = { pos_sub: 10, pos_norm: 50 }
+): Array<number> {
+ counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
+ counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
+
+ // Generating bit fields first and then converting to f16, so that the spread across the possible f16 values is more
+ // even. Generating against the bounds of f16 values directly results in the values being extremely biased towards the
+ // extremes, since they are so much larger.
+ const bit_fields = [
+ ...linearRange(kBit.f16.negative.min, kBit.f16.negative.max, counts.neg_norm),
+ ...linearRange(
+ kBit.f16.subnormal.negative.min,
+ kBit.f16.subnormal.negative.max,
+ counts.neg_sub
+ ),
+ 0,
+ ...linearRange(
+ kBit.f16.subnormal.positive.min,
+ kBit.f16.subnormal.positive.max,
+ counts.pos_sub
+ ),
+ ...linearRange(kBit.f16.positive.min, kBit.f16.positive.max, counts.pos_norm),
+ ].map(Math.trunc);
+ return bit_fields.map(hexToF16);
+}
+
+/**
+ * @returns an ascending sorted array of numbers spread over the entire range of 32-bit signed ints
+ *
+ * Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0
+ * Zero is included in range.
+ *
+ * @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater.
+ */
+export function fullI32Range(
+ counts: {
+ negative?: number;
+ positive: number;
+ } = { positive: 50 }
+): Array<number> {
+ counts.negative = counts.negative === undefined ? counts.positive : counts.negative;
+ return [
+ ...biasedRange(kValue.i32.negative.min, -1, counts.negative),
+ 0,
+ ...biasedRange(1, kValue.i32.positive.max, counts.positive),
+ ].map(Math.trunc);
+}
+
+/** Short list of u32 values of interest to test against */
+const kInterestingU32Values: number[] = [0, 1, kValue.u32.max / 2, kValue.u32.max];
+
+/** @returns minimal u32 values that cover the entire range of u32 behaviours
+ *
+ * This is used instead of fullU32Range when the number of test cases being
+ * generated is a super linear function of the length of u32 values which is
+ * leading to time outs.
+ */
+export function sparseU32Range(): number[] {
+ return kInterestingU32Values;
+}
+
+const kVectorU32Values = {
+ 2: kInterestingU32Values.flatMap(f => [
+ [f, 1],
+ [1, f],
+ ]),
+ 3: kInterestingU32Values.flatMap(f => [
+ [f, 1, 2],
+ [1, f, 2],
+ [1, 2, f],
+ ]),
+ 4: kInterestingU32Values.flatMap(f => [
+ [f, 1, 2, 3],
+ [1, f, 2, 3],
+ [1, 2, f, 3],
+ [1, 2, 3, f],
+ ]),
+};
+
+/**
+ * Returns set of vectors, indexed by dimension containing interesting u32
+ * values.
+ *
+ * The tests do not do the simple option for coverage of computing the cartesian
+ * product of all of the interesting u32 values N times for vecN tests,
+ * because that creates a huge number of tests for vec3 and vec4, leading to
+ * time outs.
+ *
+ * Instead they insert the interesting u32 values into each location of the
+ * vector to get a spread of testing over the entire range. This reduces the
+ * number of cases being run substantially, but maintains coverage.
+ */
+export function vectorU32Range(dim: number): number[][] {
+ assert(dim === 2 || dim === 3 || dim === 4, 'vectorU32Range only accepts dimensions 2, 3, and 4');
+ return kVectorU32Values[dim];
+}
+
+/**
+ * @returns an ascending sorted array of numbers spread over the entire range of 32-bit unsigned ints
+ *
+ * Numbers are biased towards 0, and 0 is included in the range.
+ *
+ * @param count number of entries to include in the range, in addition to 0, must be greater than 0, defaults to 50
+ */
+export function fullU32Range(count: number = 50): Array<number> {
+ return [0, ...biasedRange(1, kValue.u32.max, count)].map(Math.trunc);
+}
+
+/** Short list of f32 values of interest to test against */
+const kInterestingF32Values: number[] = [
+ kValue.f32.negative.min,
+ -10.0,
+ -1.0,
+ kValue.f32.negative.max,
+ kValue.f32.subnormal.negative.min,
+ kValue.f32.subnormal.negative.max,
+ 0.0,
+ kValue.f32.subnormal.positive.min,
+ kValue.f32.subnormal.positive.max,
+ kValue.f32.positive.min,
+ 1.0,
+ 10.0,
+ kValue.f32.positive.max,
+];
+
+/** @returns minimal f32 values that cover the entire range of f32 behaviours
+ *
+ * Has specially selected values that cover edge cases, normals, and subnormals.
+ * This is used instead of fullF32Range when the number of test cases being
+ * generated is a super linear function of the length of f32 values which is
+ * leading to time outs.
+ *
+ * These values have been chosen to attempt to test the widest range of f32
+ * behaviours in the lowest number of entries, so may potentially miss function
+ * specific values of interest. If there are known values of interest they
+ * should be appended to this list in the test generation code.
+ */
+export function sparseF32Range(): number[] {
+ return kInterestingF32Values;
+}
+
+const kVectorF32Values = {
+ 2: kInterestingF32Values.flatMap(f => [
+ [f, 1.0],
+ [1.0, f],
+ [f, -1.0],
+ [-1.0, f],
+ ]),
+ 3: kInterestingF32Values.flatMap(f => [
+ [f, 1.0, 2.0],
+ [1.0, f, 2.0],
+ [1.0, 2.0, f],
+ [f, -1.0, -2.0],
+ [-1.0, f, -2.0],
+ [-1.0, -2.0, f],
+ ]),
+ 4: kInterestingF32Values.flatMap(f => [
+ [f, 1.0, 2.0, 3.0],
+ [1.0, f, 2.0, 3.0],
+ [1.0, 2.0, f, 3.0],
+ [1.0, 2.0, 3.0, f],
+ [f, -1.0, -2.0, -3.0],
+ [-1.0, f, -2.0, -3.0],
+ [-1.0, -2.0, f, -3.0],
+ [-1.0, -2.0, -3.0, f],
+ ]),
+};
+
+/**
+ * Returns set of vectors, indexed by dimension containing interesting float
+ * values.
+ *
+ * The tests do not do the simple option for coverage of computing the cartesian
+ * product of all of the interesting float values N times for vecN tests,
+ * because that creates a huge number of tests for vec3 and vec4, leading to
+ * time outs.
+ *
+ * Instead they insert the interesting f32 values into each location of the
+ * vector to get a spread of testing over the entire range. This reduces the
+ * number of cases being run substantially, but maintains coverage.
+ */
+export function vectorF32Range(dim: number): number[][] {
+ assert(dim === 2 || dim === 3 || dim === 4, 'vectorF32Range only accepts dimensions 2, 3, and 4');
+ return kVectorF32Values[dim];
+}
+
+const kSparseVectorF32Values = {
+ 2: sparseF32Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
+ 3: sparseF32Range().map((f, idx) => [
+ idx % 3 === 0 ? f : idx,
+ idx % 3 === 1 ? f : -idx,
+ idx % 3 === 2 ? f : idx,
+ ]),
+ 4: sparseF32Range().map((f, idx) => [
+ idx % 4 === 0 ? f : idx,
+ idx % 4 === 1 ? f : -idx,
+ idx % 4 === 2 ? f : idx,
+ idx % 4 === 3 ? f : -idx,
+ ]),
+};
+
+/**
+ * Minimal set of vectors, indexed by dimension, that contain interesting float
+ * values.
+ *
+ * This is an even more stripped down version of `vectorF32Range` for when
+ * pairs of vectors are being tested.
+ * All of the interesting floats from sparseF32 are guaranteed to be tested, but
+ * not in every position.
+ */
+export function sparseVectorF32Range(dim: number): number[][] {
+ assert(
+ dim === 2 || dim === 3 || dim === 4,
+ 'sparseVectorF32Range only accepts dimensions 2, 3, and 4'
+ );
+ return kSparseVectorF32Values[dim];
+}
+/**
+ * @returns the result matrix in Array<Array<number>> type.
+ *
+ * Matrix multiplication. A is m x n and B is n x p. Returns
+ * m x p result.
+ */
+// A is m x n. B is n x p. product is m x p.
+export function multiplyMatrices(
+ A: Array<Array<number>>,
+ B: Array<Array<number>>
+): Array<Array<number>> {
+ assert(A.length > 0 && B.length > 0 && B[0].length > 0 && A[0].length === B.length);
+ const product = new Array<Array<number>>(A.length);
+ for (let i = 0; i < product.length; ++i) {
+ product[i] = new Array<number>(B[0].length).fill(0);
+ }
+
+ for (let m = 0; m < A.length; ++m) {
+ for (let p = 0; p < B[0].length; ++p) {
+ for (let n = 0; n < B.length; ++n) {
+ product[m][p] += A[m][n] * B[n][p];
+ }
+ }
+ }
+
+ return product;
+}
+
+/** Sign-extend the `bits`-bit number `n` to a 32-bit signed integer. */
+export function signExtend(n: number, bits: number): number {
+ const shift = 32 - bits;
+ return (n << shift) >> shift;
+}
+
+/** @returns the closest 32-bit floating point value to the input */
+export function quantizeToF32(num: number): number {
+ return f32(num).value as number;
+}
+
+/** @returns the closest 32-bit signed integer value to the input */
+export function quantizeToI32(num: number): number {
+ return i32(num).value as number;
+}
+
+/** @returns the closest 32-bit signed integer value to the input */
+export function quantizeToU32(num: number): number {
+ return u32(num).value as number;
+}
+
+/** @returns whether the number is an integer and a power of two */
+export function isPowerOfTwo(n: number): boolean {
+ if (!Number.isInteger(n)) {
+ return false;
+ }
+ return n !== 0 && (n & (n - 1)) === 0;
+}
+
+/** @returns the Greatest Common Divisor (GCD) of the inputs */
+export function gcd(a: number, b: number): number {
+ assert(Number.isInteger(a) && a > 0);
+ assert(Number.isInteger(b) && b > 0);
+
+ while (b !== 0) {
+ const bTemp = b;
+ b = a % b;
+ a = bTemp;
+ }
+
+ return a;
+}
+
+/** @returns the Least Common Multiplier (LCM) of the inputs */
+export function lcm(a: number, b: number): number {
+ return (a * b) / gcd(a, b);
+}
+
+/** Converts a 32-bit hex value to a 32-bit float value */
+export function hexToF32(hex: number): number {
+ return floatBitsToNumber(hex, kFloat32Format);
+}
+
+/** Converts a 16-bit hex value to a 16-bit float value */
+export function hexToF16(hex: number): number {
+ return floatBitsToNumber(hex, kFloat16Format);
+}
+
+/** Converts two 32-bit hex values to a 64-bit float value */
+export function hexToF64(h32: number, l32: number): number {
+ const u32Arr = new Uint32Array(2);
+ u32Arr[0] = l32;
+ u32Arr[1] = h32;
+ const f64Arr = new Float64Array(u32Arr.buffer);
+ return f64Arr[0];
+}
+
+/** @returns the cross of an array with the intermediate result of cartesianProduct
+ *
+ * @param elements array of values to cross with the intermediate result of
+ * cartesianProduct
+ * @param intermediate arrays of values representing the partial result of
+ * cartesianProduct
+ */
+function cartesianProductImpl<T>(elements: T[], intermediate: T[][]): T[][] {
+ const result: T[][] = [];
+ elements.forEach((e: T) => {
+ if (intermediate.length > 0) {
+ intermediate.forEach((i: T[]) => {
+ result.push([...i, e]);
+ });
+ } else {
+ result.push([e]);
+ }
+ });
+ return result;
+}
+
+/** @returns the cartesian product (NxMx...) of a set of arrays
+ *
+ * This is implemented by calculating the cross of a single input against an
+ * intermediate result for each input to build up the final array of arrays.
+ *
+ * There are examples of doing this more succinctly using map & reduce online,
+ * but they are a bit more opaque to read.
+ *
+ * @param inputs arrays of numbers to calculate cartesian product over
+ */
+export function cartesianProduct<T>(...inputs: T[][]): T[][] {
+ let result: T[][] = [];
+ inputs.forEach((i: T[]) => {
+ result = cartesianProductImpl<T>(i, result);
+ });
+
+ return result;
+}
+
+/** @returns all of the permutations of an array
+ *
+ * Recursively calculates all of the permutations, does not cull duplicate
+ * entries.
+ *
+ * @param input the array to get permutations of
+ */
+export function calculatePermutations<T>(input: T[]): T[][] {
+ if (input.length === 0) {
+ return [];
+ }
+
+ if (input.length === 1) {
+ return [input];
+ }
+
+ if (input.length === 2) {
+ return [input, [input[1], input[0]]];
+ }
+
+ const result: T[][] = [];
+ input.forEach((head, idx) => {
+ const tail = input.slice(0, idx).concat(input.slice(idx + 1));
+ const permutations = calculatePermutations(tail);
+ permutations.forEach(p => {
+ result.push([head, ...p]);
+ });
+ });
+
+ return result;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/memory.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/memory.ts
new file mode 100644
index 0000000000..bc5c916495
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/memory.ts
@@ -0,0 +1,25 @@
+/**
+ * Helper to exhaust VRAM until there is less than 64 MB of capacity. Returns
+ * an opaque closure which can be called to free the allocated resources later.
+ */
+export async function exhaustVramUntilUnder64MB(device: GPUDevice) {
+ const allocateUntilOom = async (device: GPUDevice, size: number) => {
+ const buffers = [];
+ for (;;) {
+ device.pushErrorScope('out-of-memory');
+ const buffer = device.createBuffer({ size, usage: GPUBufferUsage.STORAGE });
+ if (await device.popErrorScope()) {
+ return buffers;
+ }
+ buffers.push(buffer);
+ }
+ };
+
+ const kLargeChunkSize = 512 * 1024 * 1024;
+ const kSmallChunkSize = 64 * 1024 * 1024;
+ const buffers = await allocateUntilOom(device, kLargeChunkSize);
+ buffers.push(...(await allocateUntilOom(device, kSmallChunkSize)));
+ return () => {
+ buffers.forEach(buffer => buffer.destroy());
+ };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts
new file mode 100644
index 0000000000..af98ab7ecf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/pretty_diff_tables.ts
@@ -0,0 +1,51 @@
+import { range } from '../../common/util/util.js';
+
+/**
+ * Pretty-prints a "table" of cell values (each being `number | string`), right-aligned.
+ * Each row may be any iterator, including lazily-generated (potentially infinite) rows.
+ *
+ * The first argument is the printing options:
+ * - fillToWidth: Keep printing columns (as long as there is data) until this width is passed.
+ * If there is more data, "..." is appended.
+ * - numberToString: if a cell value is a number, this is used to stringify it.
+ *
+ * Each remaining argument provides one row for the table.
+ */
+export function generatePrettyTable(
+ { fillToWidth, numberToString }: { fillToWidth: number; numberToString: (n: number) => string },
+ rows: ReadonlyArray<Iterable<string | number>>
+): string {
+ const rowStrings = range(rows.length, () => '');
+ let totalTableWidth = 0;
+ const iters = rows.map(row => row[Symbol.iterator]());
+
+ // Loop over columns
+ for (;;) {
+ const cellsForColumn = iters.map(iter => {
+ const r = iter.next(); // Advance the iterator for each row, in lock-step.
+ return r.done ? undefined : typeof r.value === 'number' ? numberToString(r.value) : r.value;
+ });
+ if (cellsForColumn.every(cell => cell === undefined)) break;
+
+ // Maximum width of any cell in this column, plus one for space between columns
+ // (also inserts a space at the left of the first column).
+ const colWidth = Math.max(...cellsForColumn.map(c => (c === undefined ? 0 : c.length))) + 1;
+ for (let row = 0; row < rowStrings.length; ++row) {
+ const cell = cellsForColumn[row];
+ if (cell !== undefined) {
+ rowStrings[row] += cell.padStart(colWidth);
+ }
+ }
+
+ totalTableWidth += colWidth;
+ if (totalTableWidth >= fillToWidth) {
+ for (let row = 0; row < rowStrings.length; ++row) {
+ if (cellsForColumn[row] !== undefined) {
+ rowStrings[row] += ' ...';
+ }
+ }
+ break;
+ }
+ }
+ return rowStrings.join('\n');
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts
new file mode 100644
index 0000000000..2a09061527
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/shader.ts
@@ -0,0 +1,196 @@
+import { unreachable } from '../../common/util/util.js';
+
+export const kDefaultVertexShaderCode = `
+@vertex fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+}
+`;
+
+export const kDefaultFragmentShaderCode = `
+@fragment fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+}`;
+
+const kPlainTypeInfo = {
+ i32: {
+ suffix: '',
+ fractionDigits: 0,
+ },
+ u32: {
+ suffix: 'u',
+ fractionDigits: 0,
+ },
+ f32: {
+ suffix: '',
+ fractionDigits: 4,
+ },
+};
+
+/**
+ *
+ * @param sampleType sampleType of texture format
+ * @returns plain type compatible of the sampleType
+ */
+export function getPlainTypeInfo(sampleType: GPUTextureSampleType): keyof typeof kPlainTypeInfo {
+ switch (sampleType) {
+ case 'sint':
+ return 'i32';
+ case 'uint':
+ return 'u32';
+ case 'float':
+ case 'unfilterable-float':
+ case 'depth':
+ return 'f32';
+ default:
+ unreachable();
+ }
+}
+
+/**
+ * Build a fragment shader based on output value and types
+ * e.g. write to color target 0 a `vec4<f32>(1.0, 0.0, 1.0, 1.0)` and color target 2 a `vec2<u32>(1, 2)`
+ * ```
+ * outputs: [
+ * {
+ * values: [1, 0, 1, 1],,
+ * plainType: 'f32',
+ * componentCount: 4,
+ * },
+ * null,
+ * {
+ * values: [1, 2],
+ * plainType: 'u32',
+ * componentCount: 2,
+ * },
+ * ]
+ * ```
+ *
+ * return:
+ * ```
+ * struct Outputs {
+ * @location(0) o1 : vec4<f32>,
+ * @location(2) o3 : vec2<u32>,
+ * }
+ * @fragment fn main() -> Outputs {
+ * return Outputs(vec4<f32>(1.0, 0.0, 1.0, 1.0), vec4<u32>(1, 2));
+ * }
+ * ```
+ *
+ * If fragDepth is given there will be an extra @builtin(frag_depth) output with the specified value assigned.
+ *
+ * @param outputs the shader outputs for each location attribute
+ * @param fragDepth the shader outputs frag_depth value (optional)
+ * @returns the fragment shader string
+ */
+export function getFragmentShaderCodeWithOutput(
+ outputs: ({
+ values: readonly number[];
+ plainType: 'i32' | 'u32' | 'f32';
+ componentCount: number;
+ } | null)[],
+ fragDepth: { value: number } | null = null
+): string {
+ if (outputs.length === 0) {
+ if (fragDepth) {
+ return `
+ @fragment fn main() -> @builtin(frag_depth) f32 {
+ return ${fragDepth.value.toFixed(kPlainTypeInfo['f32'].fractionDigits)};
+ }`;
+ }
+ return `
+ @fragment fn main() {
+ }`;
+ }
+
+ const resultStrings = [] as string[];
+ let outputStructString = '';
+
+ if (fragDepth) {
+ resultStrings.push(`${fragDepth.value.toFixed(kPlainTypeInfo['f32'].fractionDigits)}`);
+ outputStructString += `@builtin(frag_depth) depth_out: f32,\n`;
+ }
+
+ for (let i = 0; i < outputs.length; i++) {
+ const o = outputs[i];
+ if (o === null) {
+ continue;
+ }
+
+ const plainType = o.plainType;
+ const { suffix, fractionDigits } = kPlainTypeInfo[plainType];
+
+ let outputType;
+ const v = o.values.map(n => n.toFixed(fractionDigits));
+ switch (o.componentCount) {
+ case 1:
+ outputType = plainType;
+ resultStrings.push(`${v[0]}${suffix}`);
+ break;
+ case 2:
+ outputType = `vec2<${plainType}>`;
+ resultStrings.push(`${outputType}(${v[0]}${suffix}, ${v[1]}${suffix})`);
+ break;
+ case 3:
+ outputType = `vec3<${plainType}>`;
+ resultStrings.push(`${outputType}(${v[0]}${suffix}, ${v[1]}${suffix}, ${v[2]}${suffix})`);
+ break;
+ case 4:
+ outputType = `vec4<${plainType}>`;
+ resultStrings.push(
+ `${outputType}(${v[0]}${suffix}, ${v[1]}${suffix}, ${v[2]}${suffix}, ${v[3]}${suffix})`
+ );
+ break;
+ default:
+ unreachable();
+ }
+
+ outputStructString += `@location(${i}) o${i} : ${outputType},\n`;
+ }
+
+ return `
+ struct Outputs {
+ ${outputStructString}
+ }
+
+ @fragment fn main() -> Outputs {
+ return Outputs(${resultStrings.join(',')});
+ }`;
+}
+
+export type TShaderStage = 'compute' | 'vertex' | 'fragment' | 'empty';
+
+/**
+ * Return a foo shader of the given stage with the given entry point
+ * @param shaderStage
+ * @param entryPoint
+ * @returns the shader string
+ */
+export function getShaderWithEntryPoint(shaderStage: TShaderStage, entryPoint: string): string {
+ let code;
+ switch (shaderStage) {
+ case 'compute': {
+ code = `@compute @workgroup_size(1) fn ${entryPoint}() {}`;
+ break;
+ }
+ case 'vertex': {
+ code = `
+ @vertex fn ${entryPoint}() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }`;
+ break;
+ }
+ case 'fragment': {
+ code = `
+ @fragment fn ${entryPoint}() -> @location(0) vec4<f32> {
+ return vec4<f32>(0.0, 1.0, 0.0, 1.0);
+ }`;
+ break;
+ }
+ case 'empty':
+ default: {
+ code = '';
+ break;
+ }
+ }
+ return code;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture.ts
new file mode 100644
index 0000000000..d26508878f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture.ts
@@ -0,0 +1,61 @@
+import { assert } from '../../common/util/util.js';
+import { kTextureFormatInfo } from '../capability_info.js';
+
+import { align } from './math.js';
+import { TexelView } from './texture/texel_view.js';
+import { reifyExtent3D } from './unions.js';
+
+/**
+ * Creates a texture with the contents of a TexelView.
+ */
+export function makeTextureWithContents(
+ device: GPUDevice,
+ texelView: TexelView,
+ desc: Omit<GPUTextureDescriptor, 'format'>
+): GPUTexture {
+ const { width, height, depthOrArrayLayers } = reifyExtent3D(desc.size);
+
+ const { bytesPerBlock, blockWidth } = kTextureFormatInfo[texelView.format];
+ // Currently unimplemented for compressed textures.
+ assert(blockWidth === 1);
+
+ // Compute bytes per row.
+ const bytesPerRow = align(bytesPerBlock * width, 256);
+
+ // Create a staging buffer to upload the texture contents.
+ const stagingBuffer = device.createBuffer({
+ mappedAtCreation: true,
+ size: bytesPerRow * height * depthOrArrayLayers,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+
+ // Write the texels into the staging buffer.
+ texelView.writeTextureData(new Uint8Array(stagingBuffer.getMappedRange()), {
+ bytesPerRow,
+ rowsPerImage: height,
+ subrectOrigin: [0, 0, 0],
+ subrectSize: [width, height, depthOrArrayLayers],
+ });
+ stagingBuffer.unmap();
+
+ // Create the texture.
+ const texture = device.createTexture({
+ ...desc,
+ format: texelView.format,
+ usage: desc.usage | GPUTextureUsage.COPY_DST,
+ });
+
+ // Copy from the staging buffer into the texture.
+ const commandEncoder = device.createCommandEncoder();
+ commandEncoder.copyBufferToTexture(
+ { buffer: stagingBuffer, bytesPerRow },
+ { texture },
+ desc.size
+ );
+ device.queue.submit([commandEncoder.finish()]);
+
+ // Clean up the staging buffer.
+ stagingBuffer.destroy();
+
+ return texture;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts
new file mode 100644
index 0000000000..49e194a7ab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/base.ts
@@ -0,0 +1,213 @@
+import { assert, unreachable } from '../../../common/util/util.js';
+import { kTextureFormatInfo } from '../../capability_info.js';
+import { align } from '../../util/math.js';
+import { reifyExtent3D } from '../../util/unions.js';
+
+/**
+ * Compute the maximum mip level count allowed for a given texture size and texture dimension.
+ */
+export function maxMipLevelCount({
+ size,
+ dimension = '2d',
+}: {
+ readonly size: Readonly<GPUExtent3DDict> | readonly number[];
+ readonly dimension?: GPUTextureDimension;
+}): number {
+ const sizeDict = reifyExtent3D(size);
+
+ let maxMippedDimension = 0;
+ switch (dimension) {
+ case '1d':
+ maxMippedDimension = 1; // No mipmaps allowed.
+ break;
+ case '2d':
+ maxMippedDimension = Math.max(sizeDict.width, sizeDict.height);
+ break;
+ case '3d':
+ maxMippedDimension = Math.max(sizeDict.width, sizeDict.height, sizeDict.depthOrArrayLayers);
+ break;
+ }
+
+ return Math.floor(Math.log2(maxMippedDimension)) + 1;
+}
+
+/**
+ * Compute the "physical size" of a mip level: the size of the level, rounded up to a
+ * multiple of the texel block size.
+ */
+export function physicalMipSize(
+ baseSize: Required<GPUExtent3DDict>,
+ format: GPUTextureFormat,
+ dimension: GPUTextureDimension,
+ level: number
+): Required<GPUExtent3DDict> {
+ switch (dimension) {
+ case '1d':
+ assert(level === 0, '1d textures cannot be mipmapped');
+ assert(baseSize.height === 1 && baseSize.depthOrArrayLayers === 1, '1d texture not Wx1x1');
+ return { width: baseSize.width, height: 1, depthOrArrayLayers: 1 };
+
+ case '2d': {
+ assert(
+ Math.max(baseSize.width, baseSize.height) >> level > 0,
+ () => `level (${level}) too large for base size (${baseSize.width}x${baseSize.height})`
+ );
+
+ const virtualWidthAtLevel = Math.max(baseSize.width >> level, 1);
+ const virtualHeightAtLevel = Math.max(baseSize.height >> level, 1);
+ const physicalWidthAtLevel = align(
+ virtualWidthAtLevel,
+ kTextureFormatInfo[format].blockWidth
+ );
+ const physicalHeightAtLevel = align(
+ virtualHeightAtLevel,
+ kTextureFormatInfo[format].blockHeight
+ );
+ return {
+ width: physicalWidthAtLevel,
+ height: physicalHeightAtLevel,
+ depthOrArrayLayers: baseSize.depthOrArrayLayers,
+ };
+ }
+
+ case '3d': {
+ assert(
+ Math.max(baseSize.width, baseSize.height, baseSize.depthOrArrayLayers) >> level > 0,
+ () =>
+ `level (${level}) too large for base size (${baseSize.width}x${baseSize.height}x${baseSize.depthOrArrayLayers})`
+ );
+ assert(
+ kTextureFormatInfo[format].blockWidth === 1 && kTextureFormatInfo[format].blockHeight === 1,
+ 'not implemented for 3d block formats'
+ );
+ return {
+ width: Math.max(baseSize.width >> level, 1),
+ height: Math.max(baseSize.height >> level, 1),
+ depthOrArrayLayers: Math.max(baseSize.depthOrArrayLayers >> level, 1),
+ };
+ }
+ }
+}
+
+/**
+ * Compute the "virtual size" of a mip level of a texture (not accounting for texel block rounding).
+ *
+ * MAINTENANCE_TODO: Change input/output to Required<GPUExtent3DDict> for consistency.
+ */
+export function virtualMipSize(
+ dimension: GPUTextureDimension,
+ size: readonly [number, number, number],
+ mipLevel: number
+): [number, number, number] {
+ const shiftMinOne = (n: number) => Math.max(1, n >> mipLevel);
+ switch (dimension) {
+ case '1d':
+ assert(size[2] === 1);
+ return [shiftMinOne(size[0]), size[1], size[2]];
+ case '2d':
+ return [shiftMinOne(size[0]), shiftMinOne(size[1]), size[2]];
+ case '3d':
+ return [shiftMinOne(size[0]), shiftMinOne(size[1]), shiftMinOne(size[2])];
+ default:
+ unreachable();
+ }
+}
+
+/**
+ * Get texture dimension from view dimension in order to create an compatible texture for a given
+ * view dimension.
+ */
+export function getTextureDimensionFromView(viewDimension: GPUTextureViewDimension) {
+ switch (viewDimension) {
+ case '1d':
+ return '1d';
+ case '2d':
+ case '2d-array':
+ case 'cube':
+ case 'cube-array':
+ return '2d';
+ case '3d':
+ return '3d';
+ default:
+ unreachable();
+ }
+}
+
+/** Returns the possible valid view dimensions for a given texture dimension. */
+export function viewDimensionsForTextureDimension(textureDimension: GPUTextureDimension) {
+ switch (textureDimension) {
+ case '1d':
+ return ['1d'] as const;
+ case '2d':
+ return ['2d', '2d-array', 'cube', 'cube-array'] as const;
+ case '3d':
+ return ['3d'] as const;
+ }
+}
+
+/** Returns the default view dimension for a given texture descriptor. */
+export function defaultViewDimensionsForTexture(textureDescriptor: Readonly<GPUTextureDescriptor>) {
+ switch (textureDescriptor.dimension) {
+ case '1d':
+ return '1d';
+ case '2d': {
+ const sizeDict = reifyExtent3D(textureDescriptor.size);
+ return sizeDict.depthOrArrayLayers > 1 ? '2d-array' : '2d';
+ }
+ case '3d':
+ return '3d';
+ default:
+ unreachable();
+ }
+}
+
+/** Reifies the optional fields of `GPUTextureDescriptor`.
+ * MAINTENANCE_TODO: viewFormats should not be omitted here, but it seems likely that the
+ * @webgpu/types definition will have to change before we can include it again.
+ */
+export function reifyTextureDescriptor(
+ desc: Readonly<GPUTextureDescriptor>
+): Required<Omit<GPUTextureDescriptor, 'label' | 'viewFormats'>> {
+ return { dimension: '2d' as const, mipLevelCount: 1, sampleCount: 1, ...desc };
+}
+
+/** Reifies the optional fields of `GPUTextureViewDescriptor` (given a `GPUTextureDescriptor`). */
+export function reifyTextureViewDescriptor(
+ textureDescriptor: Readonly<GPUTextureDescriptor>,
+ view: Readonly<GPUTextureViewDescriptor>
+): Required<Omit<GPUTextureViewDescriptor, 'label'>> {
+ const texture = reifyTextureDescriptor(textureDescriptor);
+
+ // IDL defaulting
+
+ const baseMipLevel = view.baseMipLevel ?? 0;
+ const baseArrayLayer = view.baseArrayLayer ?? 0;
+ const aspect = view.aspect ?? 'all';
+
+ // Spec defaulting
+
+ const format = view.format ?? texture.format;
+ const mipLevelCount = view.mipLevelCount ?? texture.mipLevelCount - baseMipLevel;
+ const dimension = view.dimension ?? defaultViewDimensionsForTexture(texture);
+
+ let arrayLayerCount = view.arrayLayerCount;
+ if (arrayLayerCount === undefined) {
+ if (dimension === '2d-array' || dimension === 'cube-array') {
+ arrayLayerCount = reifyExtent3D(texture.size).depthOrArrayLayers - baseArrayLayer;
+ } else if (dimension === 'cube') {
+ arrayLayerCount = 6;
+ } else {
+ arrayLayerCount = 1;
+ }
+ }
+
+ return {
+ format,
+ dimension,
+ aspect,
+ baseMipLevel,
+ mipLevelCount,
+ baseArrayLayer,
+ arrayLayerCount,
+ };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/data_generation.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/data_generation.ts
new file mode 100644
index 0000000000..7ad7d30e08
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/data_generation.ts
@@ -0,0 +1,83 @@
+/**
+ * A helper class that generates ranges of dummy data for buffer or texture operations
+ * efficiently. Tries to minimize allocations and data updates.
+ */
+export class DataArrayGenerator {
+ private dataBuffer = new Uint8Array(256);
+
+ private lastOffset = 0;
+ private lastStart = 0;
+ private lastByteSize = 0;
+
+ /** Find the nearest power of two greater than or equal to the input value. */
+ private nextPowerOfTwo(value: number) {
+ return 1 << (32 - Math.clz32(value - 1));
+ }
+
+ private generateData(byteSize: number, start: number = 0, offset: number = 0) {
+ const prevSize = this.dataBuffer.length;
+
+ if (prevSize < byteSize) {
+ // If the requested data is larger than the allocated buffer, reallocate it to a buffer large
+ // enough to handle the new request.
+ const newData = new Uint8Array(this.nextPowerOfTwo(byteSize));
+
+ if (this.lastOffset === offset && this.lastStart === start && this.lastByteSize) {
+ // Do a fast copy of any previous data that was generated.
+ newData.set(this.dataBuffer);
+ }
+
+ this.dataBuffer = newData;
+ } else if (this.lastOffset < offset) {
+ // Ensure all values up to the offset are zeroed out.
+ this.dataBuffer.fill(0, this.lastOffset, offset);
+ }
+
+ // If the offset or start values have changed, the whole data range needs to be regenerated.
+ if (this.lastOffset !== offset || this.lastStart !== start) {
+ this.lastByteSize = 0;
+ }
+
+ // Generate any new values that are required
+ if (this.lastByteSize < byteSize) {
+ for (let i = this.lastByteSize; i < byteSize - offset; ++i) {
+ this.dataBuffer[i + offset] = ((i ** 3 + i + start) % 251) + 1; // Ensure data is always non-zero
+ }
+
+ this.lastOffset = offset;
+ this.lastStart = start;
+ this.lastByteSize = byteSize;
+ }
+ }
+
+ /**
+ * Returns a new view into the generated data that's the correct length. Because this is a view
+ * previously returned views from the same generator will have their values overwritten as well.
+ * @param {number} byteSize - Number of bytes the returned view should contain.
+ * @param {number} [start] - The value of the first element generated in the view.
+ * @param {number} [offset] - Offset of the generated data within the view. Preceeding values will be 0.
+ * @returns {Uint8Array} A new Uint8Array view into the generated data.
+ */
+ generateView(byteSize: number, start: number = 0, offset: number = 0): Uint8Array {
+ this.generateData(byteSize, start, offset);
+
+ if (this.dataBuffer.length === byteSize) {
+ return this.dataBuffer;
+ }
+ return new Uint8Array(this.dataBuffer.buffer, 0, byteSize);
+ }
+
+ /**
+ * Returns a copy of the generated data. Note that this still changes the underlying buffer, so
+ * any previously generated views will still be overwritten, but the returned copy won't reflect
+ * future generate* calls.
+ * @param {number} byteSize - Number of bytes the returned array should contain.
+ * @param {number} [start] - The value of the first element generated in the view.
+ * @param {number} [offset] - Offset of the generated data within the view. Preceeding values will be 0.
+ * @returns {Uint8Array} A new Uint8Array copy of the generated data.
+ */
+ generateAndCopyView(byteSize: number, start: number = 0, offset: number = 0) {
+ this.generateData(byteSize, start, offset);
+ return this.dataBuffer.slice(0, byteSize);
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/layout.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/layout.ts
new file mode 100644
index 0000000000..e53c5d804d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/layout.ts
@@ -0,0 +1,370 @@
+import { assert, memcpy } from '../../../common/util/util.js';
+import {
+ EncodableTextureFormat,
+ kTextureFormatInfo,
+ resolvePerAspectFormat,
+ SizedTextureFormat,
+} from '../../capability_info.js';
+import { align } from '../math.js';
+import { reifyExtent3D } from '../unions.js';
+
+import { physicalMipSize, virtualMipSize } from './base.js';
+
+/** The minimum `bytesPerRow` alignment, per spec. */
+export const kBytesPerRowAlignment = 256;
+/** The minimum buffer copy alignment, per spec. */
+export const kBufferCopyAlignment = 4;
+
+/**
+ * Overridable layout options for {@link getTextureCopyLayout}.
+ */
+export interface LayoutOptions {
+ mipLevel: number;
+ bytesPerRow?: number;
+ rowsPerImage?: number;
+ aspect?: GPUTextureAspect;
+}
+
+const kDefaultLayoutOptions = {
+ mipLevel: 0,
+ bytesPerRow: undefined,
+ rowsPerImage: undefined,
+ aspect: 'all' as const,
+};
+
+/** The info returned by {@link getTextureSubCopyLayout}. */
+export interface TextureSubCopyLayout {
+ bytesPerBlock: number;
+ byteLength: number;
+ /** Number of bytes in each row, not accounting for {@link kBytesPerRowAlignment}. */
+ minBytesPerRow: number;
+ /**
+ * Actual value of bytesPerRow, defaulting to `align(minBytesPerRow, kBytesPerRowAlignment}`
+ * if not overridden.
+ */
+ bytesPerRow: number;
+ /** Actual value of rowsPerImage, defaulting to `mipSize[1]` if not overridden. */
+ rowsPerImage: number;
+}
+
+/** The info returned by {@link getTextureCopyLayout}. */
+export interface TextureCopyLayout extends TextureSubCopyLayout {
+ mipSize: [number, number, number];
+}
+
+/**
+ * Computes layout information for a copy of the whole subresource at `mipLevel` of a GPUTexture
+ * of size `baseSize` with the provided `format` and `dimension`.
+ *
+ * Computes default values for `bytesPerRow` and `rowsPerImage` if not specified.
+ *
+ * MAINTENANCE_TODO: Change input/output to Required<GPUExtent3DDict> for consistency.
+ */
+export function getTextureCopyLayout(
+ format: GPUTextureFormat,
+ dimension: GPUTextureDimension,
+ baseSize: readonly [number, number, number],
+ { mipLevel, bytesPerRow, rowsPerImage, aspect }: LayoutOptions = kDefaultLayoutOptions
+): TextureCopyLayout {
+ const mipSize = physicalMipSize(
+ { width: baseSize[0], height: baseSize[1], depthOrArrayLayers: baseSize[2] },
+ format,
+ dimension,
+ mipLevel
+ );
+
+ const layout = getTextureSubCopyLayout(format, mipSize, { bytesPerRow, rowsPerImage, aspect });
+ return { ...layout, mipSize: [mipSize.width, mipSize.height, mipSize.depthOrArrayLayers] };
+}
+
+/**
+ * Computes layout information for a copy of size `copySize` to/from a GPUTexture with the provided
+ * `format`.
+ *
+ * Computes default values for `bytesPerRow` and `rowsPerImage` if not specified.
+ */
+export function getTextureSubCopyLayout(
+ format: GPUTextureFormat,
+ copySize: GPUExtent3D,
+ {
+ bytesPerRow,
+ rowsPerImage,
+ aspect = 'all' as const,
+ }: {
+ readonly bytesPerRow?: number;
+ readonly rowsPerImage?: number;
+ readonly aspect?: GPUTextureAspect;
+ } = {}
+): TextureSubCopyLayout {
+ format = resolvePerAspectFormat(format, aspect);
+ const { blockWidth, blockHeight, bytesPerBlock } = kTextureFormatInfo[format];
+ assert(bytesPerBlock !== undefined);
+
+ const copySize_ = reifyExtent3D(copySize);
+ assert(
+ copySize_.width > 0 && copySize_.height > 0 && copySize_.depthOrArrayLayers > 0,
+ 'not implemented for empty copySize'
+ );
+ assert(
+ copySize_.width % blockWidth === 0 && copySize_.height % blockHeight === 0,
+ 'copySize must be a multiple of the block size'
+ );
+ const copySizeBlocks = {
+ width: copySize_.width / blockWidth,
+ height: copySize_.height / blockHeight,
+ depthOrArrayLayers: copySize_.depthOrArrayLayers,
+ };
+
+ const minBytesPerRow = copySizeBlocks.width * bytesPerBlock;
+ const alignedMinBytesPerRow = align(minBytesPerRow, kBytesPerRowAlignment);
+ if (bytesPerRow !== undefined) {
+ assert(bytesPerRow >= alignedMinBytesPerRow);
+ assert(bytesPerRow % kBytesPerRowAlignment === 0);
+ } else {
+ bytesPerRow = alignedMinBytesPerRow;
+ }
+
+ if (rowsPerImage !== undefined) {
+ assert(rowsPerImage >= copySizeBlocks.height);
+ } else {
+ rowsPerImage = copySizeBlocks.height;
+ }
+
+ const bytesPerSlice = bytesPerRow * rowsPerImage;
+ const sliceSize =
+ bytesPerRow * (copySizeBlocks.height - 1) + bytesPerBlock * copySizeBlocks.width;
+ const byteLength = bytesPerSlice * (copySizeBlocks.depthOrArrayLayers - 1) + sliceSize;
+
+ return {
+ bytesPerBlock,
+ byteLength: align(byteLength, kBufferCopyAlignment),
+ minBytesPerRow,
+ bytesPerRow,
+ rowsPerImage,
+ };
+}
+
+/**
+ * Fill an ArrayBuffer with the linear-memory representation of a solid-color
+ * texture where every texel has the byte value `texelValue`.
+ * Preserves the contents of `outputBuffer` which are in "padding" space between image rows.
+ *
+ * Effectively emulates a copyTextureToBuffer from a solid-color texture to a buffer.
+ */
+export function fillTextureDataWithTexelValue(
+ texelValue: ArrayBuffer,
+ format: EncodableTextureFormat,
+ dimension: GPUTextureDimension,
+ outputBuffer: ArrayBuffer,
+ size: [number, number, number],
+ options: LayoutOptions = kDefaultLayoutOptions
+): void {
+ const { blockWidth, blockHeight, bytesPerBlock } = kTextureFormatInfo[format];
+ // Block formats are not handled correctly below.
+ assert(blockWidth === 1);
+ assert(blockHeight === 1);
+
+ assert(bytesPerBlock === texelValue.byteLength, 'texelValue must be of size bytesPerBlock');
+
+ const { byteLength, rowsPerImage, bytesPerRow } = getTextureCopyLayout(
+ format,
+ dimension,
+ size,
+ options
+ );
+
+ assert(byteLength <= outputBuffer.byteLength);
+
+ const mipSize = virtualMipSize(dimension, size, options.mipLevel);
+
+ const outputTexelValueBytes = new Uint8Array(outputBuffer);
+ for (let slice = 0; slice < mipSize[2]; ++slice) {
+ for (let row = 0; row < mipSize[1]; row += blockHeight) {
+ for (let col = 0; col < mipSize[0]; col += blockWidth) {
+ const byteOffset =
+ slice * rowsPerImage * bytesPerRow + row * bytesPerRow + col * texelValue.byteLength;
+ memcpy({ src: texelValue }, { dst: outputTexelValueBytes, start: byteOffset });
+ }
+ }
+ }
+}
+
+/**
+ * Create a `COPY_SRC` GPUBuffer containing the linear-memory representation of a solid-color
+ * texture where every texel has the byte value `texelValue`.
+ */
+export function createTextureUploadBuffer(
+ texelValue: ArrayBuffer,
+ device: GPUDevice,
+ format: EncodableTextureFormat,
+ dimension: GPUTextureDimension,
+ size: [number, number, number],
+ options: LayoutOptions = kDefaultLayoutOptions
+): {
+ buffer: GPUBuffer;
+ bytesPerRow: number;
+ rowsPerImage: number;
+} {
+ const { byteLength, bytesPerRow, rowsPerImage, bytesPerBlock } = getTextureCopyLayout(
+ format,
+ dimension,
+ size,
+ options
+ );
+
+ const buffer = device.createBuffer({
+ mappedAtCreation: true,
+ size: byteLength,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ const mapping = buffer.getMappedRange();
+
+ assert(texelValue.byteLength === bytesPerBlock);
+ fillTextureDataWithTexelValue(texelValue, format, dimension, mapping, size, options);
+ buffer.unmap();
+
+ return {
+ buffer,
+ bytesPerRow,
+ rowsPerImage,
+ };
+}
+
+export type ImageCopyType = 'WriteTexture' | 'CopyB2T' | 'CopyT2B';
+export const kImageCopyTypes: readonly ImageCopyType[] = [
+ 'WriteTexture',
+ 'CopyB2T',
+ 'CopyT2B',
+] as const;
+
+/**
+ * Computes `bytesInACompleteRow` (as defined by the WebGPU spec) for image copies (B2T/T2B/writeTexture).
+ */
+export function bytesInACompleteRow(copyWidth: number, format: SizedTextureFormat): number {
+ const info = kTextureFormatInfo[format];
+ assert(copyWidth % info.blockWidth === 0);
+ return (info.bytesPerBlock * copyWidth) / info.blockWidth;
+}
+
+function validateBytesPerRow({
+ bytesPerRow,
+ bytesInLastRow,
+ sizeInBlocks,
+}: {
+ bytesPerRow: number | undefined;
+ bytesInLastRow: number;
+ sizeInBlocks: Required<GPUExtent3DDict>;
+}) {
+ // If specified, layout.bytesPerRow must be greater than or equal to bytesInLastRow.
+ if (bytesPerRow !== undefined && bytesPerRow < bytesInLastRow) {
+ return false;
+ }
+ // If heightInBlocks > 1, layout.bytesPerRow must be specified.
+ // If copyExtent.depthOrArrayLayers > 1, layout.bytesPerRow and layout.rowsPerImage must be specified.
+ if (
+ bytesPerRow === undefined &&
+ (sizeInBlocks.height > 1 || sizeInBlocks.depthOrArrayLayers > 1)
+ ) {
+ return false;
+ }
+ return true;
+}
+
+function validateRowsPerImage({
+ rowsPerImage,
+ sizeInBlocks,
+}: {
+ rowsPerImage: number | undefined;
+ sizeInBlocks: Required<GPUExtent3DDict>;
+}) {
+ // If specified, layout.rowsPerImage must be greater than or equal to heightInBlocks.
+ if (rowsPerImage !== undefined && rowsPerImage < sizeInBlocks.height) {
+ return false;
+ }
+ // If copyExtent.depthOrArrayLayers > 1, layout.bytesPerRow and layout.rowsPerImage must be specified.
+ if (rowsPerImage === undefined && sizeInBlocks.depthOrArrayLayers > 1) {
+ return false;
+ }
+ return true;
+}
+
+interface DataBytesForCopyArgs {
+ layout: GPUImageDataLayout;
+ format: SizedTextureFormat;
+ copySize: Readonly<GPUExtent3DDict> | readonly number[];
+ method: ImageCopyType;
+}
+
+/**
+ * Validate a copy and compute the number of bytes it needs. Throws if the copy is invalid.
+ */
+export function dataBytesForCopyOrFail(args: DataBytesForCopyArgs): number {
+ const { minDataSizeOrOverestimate, copyValid } = dataBytesForCopyOrOverestimate(args);
+ assert(copyValid, 'copy was invalid');
+ return minDataSizeOrOverestimate;
+}
+
+/**
+ * Validate a copy and compute the number of bytes it needs. If the copy is invalid, attempts to
+ * "conservatively guess" (overestimate) the number of bytes that could be needed for a copy, even
+ * if the copy parameters turn out to be invalid. This hopes to avoid "buffer too small" validation
+ * errors when attempting to test other validation errors.
+ */
+export function dataBytesForCopyOrOverestimate({
+ layout,
+ format,
+ copySize: copySize_,
+ method,
+}: DataBytesForCopyArgs): { minDataSizeOrOverestimate: number; copyValid: boolean } {
+ const copyExtent = reifyExtent3D(copySize_);
+
+ const info = kTextureFormatInfo[format];
+ assert(copyExtent.width % info.blockWidth === 0);
+ assert(copyExtent.height % info.blockHeight === 0);
+ const sizeInBlocks = {
+ width: copyExtent.width / info.blockWidth,
+ height: copyExtent.height / info.blockHeight,
+ depthOrArrayLayers: copyExtent.depthOrArrayLayers,
+ } as const;
+ const bytesInLastRow = sizeInBlocks.width * info.bytesPerBlock;
+
+ let valid = true;
+ const offset = layout.offset ?? 0;
+ if (method !== 'WriteTexture') {
+ if (offset % info.bytesPerBlock !== 0) valid = false;
+ if (layout.bytesPerRow && layout.bytesPerRow % 256 !== 0) valid = false;
+ }
+
+ let requiredBytesInCopy = 0;
+ {
+ let { bytesPerRow, rowsPerImage } = layout;
+
+ // If bytesPerRow or rowsPerImage is invalid, guess a value for the sake of various tests that
+ // don't actually care about the exact value.
+ // (In particular for validation tests that want to test invalid bytesPerRow or rowsPerImage but
+ // need to make sure the total buffer size is still big enough.)
+ if (!validateBytesPerRow({ bytesPerRow, bytesInLastRow, sizeInBlocks })) {
+ bytesPerRow = undefined;
+ valid = false;
+ }
+ if (!validateRowsPerImage({ rowsPerImage, sizeInBlocks })) {
+ rowsPerImage = undefined;
+ valid = false;
+ }
+ // Pick values for cases when (a) bpr/rpi was invalid or (b) they're validly undefined.
+ bytesPerRow ??= align(info.bytesPerBlock * sizeInBlocks.width, 256);
+ rowsPerImage ??= sizeInBlocks.height;
+
+ if (copyExtent.depthOrArrayLayers > 1) {
+ const bytesPerImage = bytesPerRow * rowsPerImage;
+ const bytesBeforeLastImage = bytesPerImage * (copyExtent.depthOrArrayLayers - 1);
+ requiredBytesInCopy += bytesBeforeLastImage;
+ }
+ if (copyExtent.depthOrArrayLayers > 0) {
+ if (sizeInBlocks.height > 1) requiredBytesInCopy += bytesPerRow * (sizeInBlocks.height - 1);
+ if (sizeInBlocks.height > 0) requiredBytesInCopy += bytesInLastRow;
+ }
+ }
+
+ return { minDataSizeOrOverestimate: offset + requiredBytesInCopy, copyValid: valid };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/subresource.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/subresource.ts
new file mode 100644
index 0000000000..b8d6e3eb21
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/subresource.ts
@@ -0,0 +1,68 @@
+/** A range of indices expressed as `{ begin, count }`. */
+export interface BeginCountRange {
+ begin: number;
+ count: number;
+}
+
+/* A range of indices, expressed as `{ begin, end }`. */
+export interface BeginEndRange {
+ begin: number;
+ end: number;
+}
+
+function endOfRange(r: BeginEndRange | BeginCountRange): number {
+ return 'count' in r ? r.begin + r.count : r.end;
+}
+
+function* rangeAsIterator(r: BeginEndRange | BeginCountRange): Generator<number> {
+ for (let i = r.begin; i < endOfRange(r); ++i) {
+ yield i;
+ }
+}
+
+/**
+ * Represents a range of subresources of a single-plane texture:
+ * a min/max mip level and min/max array layer.
+ */
+export class SubresourceRange {
+ readonly mipRange: BeginEndRange;
+ readonly layerRange: BeginEndRange;
+
+ constructor(subresources: {
+ mipRange: BeginEndRange | BeginCountRange;
+ layerRange: BeginEndRange | BeginCountRange;
+ }) {
+ this.mipRange = {
+ begin: subresources.mipRange.begin,
+ end: endOfRange(subresources.mipRange),
+ };
+ this.layerRange = {
+ begin: subresources.layerRange.begin,
+ end: endOfRange(subresources.layerRange),
+ };
+ }
+
+ /**
+ * Iterates over the "rectangle" of `{ level, layer }` pairs represented by the range.
+ */
+ *each(): Generator<{ level: number; layer: number }> {
+ for (let level = this.mipRange.begin; level < this.mipRange.end; ++level) {
+ for (let layer = this.layerRange.begin; layer < this.layerRange.end; ++layer) {
+ yield { level, layer };
+ }
+ }
+ }
+
+ /**
+ * Iterates over the mip levels represented by the range, each level including an iterator
+ * over the array layers at that level.
+ */
+ *mipLevels(): Generator<{ level: number; layers: Generator<number> }> {
+ for (let level = this.mipRange.begin; level < this.mipRange.end; ++level) {
+ yield {
+ level,
+ layers: rangeAsIterator(this.layerRange),
+ };
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts
new file mode 100644
index 0000000000..668c9098dd
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.spec.ts
@@ -0,0 +1,349 @@
+export const description = 'Test helpers for texel data produce the expected data in the shader';
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import {
+ kEncodableTextureFormats,
+ kTextureFormatInfo,
+ EncodableTextureFormat,
+} from '../../capability_info.js';
+import { GPUTest } from '../../gpu_test.js';
+
+import {
+ kTexelRepresentationInfo,
+ getSingleDataType,
+ getComponentReadbackTraits,
+} from './texel_data.js';
+
+export const g = makeTestGroup(GPUTest);
+
+function doTest(
+ t: GPUTest & {
+ params: {
+ format: EncodableTextureFormat;
+ componentData: {
+ R?: number;
+ G?: number;
+ B?: number;
+ A?: number;
+ };
+ };
+ }
+) {
+ const { format } = t.params;
+ const componentData = t.params.componentData;
+
+ const rep = kTexelRepresentationInfo[format];
+ const texelData = rep.pack(componentData);
+ const texture = t.device.createTexture({
+ format,
+ size: [1, 1, 1],
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
+ });
+
+ t.device.queue.writeTexture(
+ { texture },
+ texelData,
+ {
+ bytesPerRow: texelData.byteLength,
+ },
+ [1]
+ );
+
+ const { ReadbackTypedArray, shaderType } = getComponentReadbackTraits(getSingleDataType(format));
+
+ const shader = `
+ @group(0) @binding(0) var tex : texture_2d<${shaderType}>;
+
+ struct Output {
+ ${rep.componentOrder.map(C => `result${C} : ${shaderType},`).join('\n')}
+ };
+ @group(0) @binding(1) var<storage, read_write> output : Output;
+
+ @compute @workgroup_size(1)
+ fn main() {
+ var texel : vec4<${shaderType}> = textureLoad(tex, vec2<i32>(0, 0), 0);
+ ${rep.componentOrder.map(C => `output.result${C} = texel.${C.toLowerCase()};`).join('\n')}
+ return;
+ }`;
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: shader,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const outputBuffer = t.device.createBuffer({
+ size: rep.componentOrder.length * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: texture.createView(),
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(
+ outputBuffer,
+ new ReadbackTypedArray(
+ rep.componentOrder.map(c => {
+ const value = rep.decode(componentData)[c];
+ assert(value !== undefined);
+ return value;
+ })
+ )
+ );
+}
+
+// Make a test parameter by mapping a format and each component to a texel component
+// data value.
+function makeParam(
+ format: EncodableTextureFormat,
+ fn: (bitLength: number, index: number) => number
+) {
+ const rep = kTexelRepresentationInfo[format];
+ return {
+ R: rep.componentInfo.R ? fn(rep.componentInfo.R.bitLength, 0) : undefined,
+ G: rep.componentInfo.G ? fn(rep.componentInfo.G.bitLength, 1) : undefined,
+ B: rep.componentInfo.B ? fn(rep.componentInfo.B.bitLength, 2) : undefined,
+ A: rep.componentInfo.A ? fn(rep.componentInfo.A.bitLength, 3) : undefined,
+ };
+}
+
+g.test('unorm_texel_data_in_shader')
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'unorm'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+ makeParam(format, bitLength => max(bitLength)),
+
+ // Test a middle value
+ makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
+
+ // Test mixed values
+ makeParam(format, (bitLength, i) => {
+ const offset = [0.13, 0.63, 0.42, 0.89];
+ return Math.floor(offset[i] * max(bitLength));
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
+
+g.test('snorm_texel_data_in_shader')
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'snorm'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+ makeParam(format, bitLength => max(bitLength)),
+ makeParam(format, bitLength => -max(bitLength)),
+ makeParam(format, bitLength => -max(bitLength) - 1),
+
+ // Test a middle value
+ makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
+
+ // Test mixed values
+ makeParam(format, (bitLength, i) => {
+ const offset = [0.13, 0.63, 0.42, 0.89];
+ const range = 2 * max(bitLength);
+ return -max(bitLength) + Math.floor(offset[i] * range);
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
+
+g.test('uint_texel_data_in_shader')
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'uint'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+ makeParam(format, bitLength => max(bitLength)),
+
+ // Test a middle value
+ makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
+
+ // Test mixed values
+ makeParam(format, (bitLength, i) => {
+ const offset = [0.13, 0.63, 0.42, 0.89];
+ return Math.floor(offset[i] * max(bitLength));
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
+
+g.test('sint_texel_data_in_shader')
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'sint'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+ makeParam(format, bitLength => max(bitLength)),
+ makeParam(format, bitLength => -max(bitLength) - 1),
+
+ // Test a middle value
+ makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),
+
+ // Test mixed values
+ makeParam(format, (bitLength, i) => {
+ const offset = [0.13, 0.63, 0.42, 0.89];
+ const range = 2 * max(bitLength);
+ return -max(bitLength) + Math.floor(offset[i] * range);
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
+
+g.test('float_texel_data_in_shader')
+ .desc(
+ `
+TODO: Test NaN, Infinity, -Infinity [1]`
+ )
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'float'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+
+ // [1]: Test NaN, Infinity, -Infinity
+
+ // Test some values
+ makeParam(format, () => 0.1199951171875),
+ makeParam(format, () => 1.4072265625),
+ makeParam(format, () => 24928),
+ makeParam(format, () => -0.1319580078125),
+ makeParam(format, () => -323.25),
+ makeParam(format, () => -7440),
+
+ // Test mixed values
+ makeParam(format, (bitLength, i) => {
+ return [24896, -0.1319580078125, -323.25, -234.375][i];
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
+
+g.test('ufloat_texel_data_in_shader')
+ .desc(
+ `
+TODO: Test NaN, Infinity [1]`
+ )
+ .params(u =>
+ u
+ .combine('format', kEncodableTextureFormats)
+ .filter(({ format }) => {
+ return (
+ kTextureFormatInfo[format].copyDst &&
+ kTextureFormatInfo[format].color &&
+ getSingleDataType(format) === 'ufloat'
+ );
+ })
+ .beginSubcases()
+ .expand('componentData', ({ format }) => {
+ return [
+ // Test extrema
+ makeParam(format, () => 0),
+
+ // [2]: Test NaN, Infinity
+
+ // Test some values
+ makeParam(format, () => 0.119140625),
+ makeParam(format, () => 1.40625),
+ makeParam(format, () => 24896),
+
+ // Test scattered mixed values
+ makeParam(format, (bitLength, i) => {
+ return [24896, 1.40625, 0.119140625, 0.23095703125][i];
+ }),
+
+ // Test mixed values that are close in magnitude.
+ makeParam(format, (bitLength, i) => {
+ return [0.1337890625, 0.17919921875, 0.119140625, 0.125][i];
+ }),
+ ];
+ })
+ )
+ .fn(doTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts
new file mode 100644
index 0000000000..9651f336eb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_data.ts
@@ -0,0 +1,918 @@
+import { assert, unreachable } from '../../../common/util/util.js';
+import { EncodableTextureFormat, UncompressedTextureFormat } from '../../capability_info.js';
+import {
+ assertInIntegerRange,
+ float32ToFloatBits,
+ float32ToFloat16Bits,
+ floatAsNormalizedInteger,
+ gammaCompress,
+ gammaDecompress,
+ normalizedIntegerAsFloat,
+ packRGB9E5UFloat,
+ floatBitsToNumber,
+ float16BitsToFloat32,
+ floatBitsToNormalULPFromZero,
+ kFloat32Format,
+ kFloat16Format,
+ numberToFloat32Bits,
+ float32BitsToNumber,
+ numberToFloatBits,
+} from '../conversion.js';
+import { clamp, signExtend } from '../math.js';
+
+/** A component of a texture format: R, G, B, A, Depth, or Stencil. */
+export const enum TexelComponent {
+ R = 'R',
+ G = 'G',
+ B = 'B',
+ A = 'A',
+ Depth = 'Depth',
+ Stencil = 'Stencil',
+}
+
+/** Arbitrary data, per component of a texel format. */
+export type PerTexelComponent<T> = { [c in TexelComponent]?: T };
+
+/** How a component is encoded in its bit range of a texel format. */
+export type ComponentDataType = 'uint' | 'sint' | 'unorm' | 'snorm' | 'float' | 'ufloat' | null;
+
+/**
+ * Maps component values to component values
+ * @param {PerTexelComponent<number>} components - The input components.
+ * @returns {PerTexelComponent<number>} The new output components.
+ */
+type ComponentMapFn = (components: PerTexelComponent<number>) => PerTexelComponent<number>;
+
+/**
+ * Packs component values as an ArrayBuffer
+ * @param {PerTexelComponent<number>} components - The input components.
+ * @returns {ArrayBuffer} The packed data.
+ */
+type ComponentPackFn = (components: PerTexelComponent<number>) => ArrayBuffer;
+
+/** Unpacks component values from a Uint8Array */
+type ComponentUnpackFn = (data: Uint8Array) => PerTexelComponent<number>;
+
+/**
+ * Create a PerTexelComponent object filled with the same value for all components.
+ * @param {TexelComponent[]} components - The component names.
+ * @param {T} value - The value to assign to each component.
+ * @returns {PerTexelComponent<T>}
+ */
+function makePerTexelComponent<T>(components: TexelComponent[], value: T): PerTexelComponent<T> {
+ const values: PerTexelComponent<T> = {};
+ for (const c of components) {
+ values[c] = value;
+ }
+ return values;
+}
+
+/**
+ * Create a function which applies clones a `PerTexelComponent<number>` and then applies the
+ * function `fn` to each component of `components`.
+ * @param {(value: number) => number} fn - The mapping function to apply to component values.
+ * @param {TexelComponent[]} components - The component names.
+ * @returns {ComponentMapFn} The map function which clones the input component values, and applies
+ * `fn` to each of component of `components`.
+ */
+function applyEach(fn: (value: number) => number, components: TexelComponent[]): ComponentMapFn {
+ return (values: PerTexelComponent<number>) => {
+ values = Object.assign({}, values);
+ for (const c of components) {
+ assert(values[c] !== undefined);
+ values[c] = fn(values[c]!);
+ }
+ return values;
+ };
+}
+
+/**
+ * A `ComponentMapFn` for encoding sRGB.
+ * @param {PerTexelComponent<number>} components - The input component values.
+ * @returns {TexelComponent<number>} Gamma-compressed copy of `components`.
+ */
+const encodeSRGB: ComponentMapFn = components => {
+ assert(
+ components.R !== undefined && components.G !== undefined && components.B !== undefined,
+ 'sRGB requires all of R, G, and B components'
+ );
+ return applyEach(gammaCompress, kRGB)(components);
+};
+
+/**
+ * A `ComponentMapFn` for decoding sRGB.
+ * @param {PerTexelComponent<number>} components - The input component values.
+ * @returns {TexelComponent<number>} Gamma-decompressed copy of `components`.
+ */
+const decodeSRGB: ComponentMapFn = components => {
+ components = Object.assign({}, components);
+ assert(
+ components.R !== undefined && components.G !== undefined && components.B !== undefined,
+ 'sRGB requires all of R, G, and B components'
+ );
+ return applyEach(gammaDecompress, kRGB)(components);
+};
+
+/**
+ * Makes a `ComponentMapFn` for clamping values to the specified range.
+ */
+export function makeClampToRange(format: EncodableTextureFormat): ComponentMapFn {
+ const repr = kTexelRepresentationInfo[format];
+ assert(repr.numericRange !== null, 'Format has unknown numericRange');
+ return applyEach(x => clamp(x, repr.numericRange!), repr.componentOrder);
+}
+
+// MAINTENANCE_TODO: Look into exposing this map to the test fixture so that it can be GCed at the
+// end of each test group. That would allow for caching of larger buffers (though it's unclear how
+// ofter larger buffers are used by packComponents.)
+const smallComponentDataViews = new Map();
+function getComponentDataView(byteLength: number): DataView {
+ if (byteLength > 32) {
+ const buffer = new ArrayBuffer(byteLength);
+ return new DataView(buffer);
+ }
+ let dataView = smallComponentDataViews.get(byteLength);
+ if (!dataView) {
+ const buffer = new ArrayBuffer(byteLength);
+ dataView = new DataView(buffer);
+ smallComponentDataViews.set(byteLength, dataView);
+ }
+ return dataView;
+}
+
+/**
+ * Helper function to pack components as an ArrayBuffer.
+ * @param {TexelComponent[]} componentOrder - The order of the component data.
+ * @param {PerTexelComponent<number>} components - The input component values.
+ * @param {number | PerTexelComponent<number>} bitLengths - The length in bits of each component.
+ * If a single number, all components are the same length, otherwise this is a dictionary of
+ * per-component bit lengths.
+ * @param {ComponentDataType | PerTexelComponent<ComponentDataType>} componentDataTypes -
+ * The type of the data in `components`. If a single value, all components have the same value.
+ * Otherwise, this is a dictionary of per-component data types.
+ * @returns {ArrayBuffer} The packed component data.
+ */
+function packComponents(
+ componentOrder: TexelComponent[],
+ components: PerTexelComponent<number>,
+ bitLengths: number | PerTexelComponent<number>,
+ componentDataTypes: ComponentDataType | PerTexelComponent<ComponentDataType>
+): ArrayBuffer {
+ let bitLengthMap;
+ let totalBitLength;
+ if (typeof bitLengths === 'number') {
+ bitLengthMap = makePerTexelComponent(componentOrder, bitLengths);
+ totalBitLength = bitLengths * componentOrder.length;
+ } else {
+ bitLengthMap = bitLengths;
+ totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => {
+ assert(value !== undefined);
+ return acc + value;
+ }, 0);
+ }
+ assert(totalBitLength % 8 === 0);
+
+ const componentDataTypeMap =
+ typeof componentDataTypes === 'string' || componentDataTypes === null
+ ? makePerTexelComponent(componentOrder, componentDataTypes)
+ : componentDataTypes;
+
+ const dataView = getComponentDataView(totalBitLength / 8);
+ let bitOffset = 0;
+ for (const c of componentOrder) {
+ const value = components[c];
+ const type = componentDataTypeMap[c];
+ const bitLength = bitLengthMap[c];
+ assert(value !== undefined);
+ assert(type !== undefined);
+ assert(bitLength !== undefined);
+
+ const byteOffset = Math.floor(bitOffset / 8);
+ const byteLength = Math.ceil(bitLength / 8);
+ switch (type) {
+ case 'uint':
+ case 'unorm':
+ if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) {
+ switch (byteLength) {
+ case 1:
+ dataView.setUint8(byteOffset, value);
+ break;
+ case 2:
+ dataView.setUint16(byteOffset, value, true);
+ break;
+ case 4:
+ dataView.setUint32(byteOffset, value, true);
+ break;
+ default:
+ unreachable();
+ }
+ } else {
+ // Packed representations are all 32-bit and use Uint as the data type.
+ // ex.) rg10b11float, rgb10a2unorm
+ switch (dataView.byteLength) {
+ case 4: {
+ const currentValue = dataView.getUint32(0, true);
+
+ let mask = 0xffffffff;
+ const bitsToClearRight = bitOffset;
+ const bitsToClearLeft = 32 - (bitLength + bitOffset);
+
+ mask = (mask >>> bitsToClearRight) << bitsToClearRight;
+ mask = (mask << bitsToClearLeft) >>> bitsToClearLeft;
+
+ const newValue = (currentValue & ~mask) | (value << bitOffset);
+
+ dataView.setUint32(0, newValue, true);
+ break;
+ }
+ default:
+ unreachable();
+ }
+ }
+ break;
+ case 'sint':
+ case 'snorm':
+ assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8);
+ switch (byteLength) {
+ case 1:
+ dataView.setInt8(byteOffset, value);
+ break;
+ case 2:
+ dataView.setInt16(byteOffset, value, true);
+ break;
+ case 4:
+ dataView.setInt32(byteOffset, value, true);
+ break;
+ default:
+ unreachable();
+ }
+ break;
+ case 'float':
+ assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8);
+ switch (byteLength) {
+ case 4:
+ dataView.setFloat32(byteOffset, value, true);
+ break;
+ default:
+ unreachable();
+ }
+ break;
+ case 'ufloat':
+ case null:
+ unreachable();
+ }
+
+ bitOffset += bitLength;
+ }
+
+ return dataView.buffer;
+}
+
+/**
+ * Unpack substrings of bits from a Uint8Array, e.g. [8,8,8,8] or [9,9,9,5].
+ */
+function unpackComponentsBits(
+ componentOrder: TexelComponent[],
+ byteView: Uint8Array,
+ bitLengths: number | PerTexelComponent<number>
+): PerTexelComponent<number> {
+ const components = makePerTexelComponent(componentOrder, 0);
+
+ let bitLengthMap;
+ let totalBitLength;
+ if (typeof bitLengths === 'number') {
+ let index = 0;
+ // Optimized cases for when the bit lengths are all a well aligned value.
+ switch (bitLengths) {
+ case 8:
+ for (const c of componentOrder) {
+ components[c] = byteView[index++];
+ }
+ return components;
+ case 16: {
+ const shortView = new Uint16Array(byteView.buffer, byteView.byteOffset);
+ for (const c of componentOrder) {
+ components[c] = shortView[index++];
+ }
+ return components;
+ }
+ case 32: {
+ const longView = new Uint32Array(byteView.buffer, byteView.byteOffset);
+ for (const c of componentOrder) {
+ components[c] = longView[index++];
+ }
+ return components;
+ }
+ }
+
+ bitLengthMap = makePerTexelComponent(componentOrder, bitLengths);
+ totalBitLength = bitLengths * componentOrder.length;
+ } else {
+ bitLengthMap = bitLengths;
+ totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => {
+ assert(value !== undefined);
+ return acc + value;
+ }, 0);
+ }
+
+ assert(totalBitLength % 8 === 0);
+
+ const dataView = new DataView(byteView.buffer, byteView.byteOffset, byteView.byteLength);
+ let bitOffset = 0;
+ for (const c of componentOrder) {
+ const bitLength = bitLengthMap[c];
+ assert(bitLength !== undefined);
+
+ let value: number;
+
+ const byteOffset = Math.floor(bitOffset / 8);
+ const byteLength = Math.ceil(bitLength / 8);
+ if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) {
+ switch (byteLength) {
+ case 1:
+ value = dataView.getUint8(byteOffset);
+ break;
+ case 2:
+ value = dataView.getUint16(byteOffset, true);
+ break;
+ case 4:
+ value = dataView.getUint32(byteOffset, true);
+ break;
+ default:
+ unreachable();
+ }
+ } else {
+ // Packed representations are all 32-bit and use Uint as the data type.
+ // ex.) rg10b11float, rgb10a2unorm
+ assert(dataView.byteLength === 4);
+ const word = dataView.getUint32(0, true);
+ value = (word >>> bitOffset) & ((1 << bitLength) - 1);
+ }
+
+ bitOffset += bitLength;
+ components[c] = value;
+ }
+
+ return components;
+}
+
+/**
+ * Create an entry in `kTexelRepresentationInfo` for normalized integer texel data with constant
+ * bitlength.
+ * @param {TexelComponent[]} componentOrder - The order of the component data.
+ * @param {number} bitLength - The number of bits in each component.
+ * @param {{signed: boolean; sRGB: boolean}} opt - Boolean flags for `signed` and `sRGB`.
+ */
+function makeNormalizedInfo(
+ componentOrder: TexelComponent[],
+ bitLength: number,
+ opt: { signed: boolean; sRGB: boolean }
+): TexelRepresentationInfo {
+ const encodeNonSRGB = applyEach(
+ (n: number) => floatAsNormalizedInteger(n, bitLength, opt.signed),
+ componentOrder
+ );
+ const decodeNonSRGB = applyEach(
+ (n: number) => normalizedIntegerAsFloat(n, bitLength, opt.signed),
+ componentOrder
+ );
+
+ const numberToBitsNonSRGB = applyEach(
+ n => floatAsNormalizedInteger(n, bitLength, opt.signed),
+ componentOrder
+ );
+ let bitsToNumberNonSRGB: ComponentMapFn;
+ if (opt.signed) {
+ bitsToNumberNonSRGB = applyEach(
+ n => normalizedIntegerAsFloat(signExtend(n, bitLength), bitLength, opt.signed),
+ componentOrder
+ );
+ } else {
+ bitsToNumberNonSRGB = applyEach(
+ n => normalizedIntegerAsFloat(n, bitLength, opt.signed),
+ componentOrder
+ );
+ }
+
+ let encode: ComponentMapFn;
+ let decode: ComponentMapFn;
+ let numberToBits: ComponentMapFn;
+ let bitsToNumber: ComponentMapFn;
+ if (opt.sRGB) {
+ encode = components => encodeNonSRGB(encodeSRGB(components));
+ decode = components => decodeSRGB(decodeNonSRGB(components));
+ numberToBits = components => numberToBitsNonSRGB(encodeSRGB(components));
+ bitsToNumber = components => decodeSRGB(bitsToNumberNonSRGB(components));
+ } else {
+ encode = encodeNonSRGB;
+ decode = decodeNonSRGB;
+ numberToBits = numberToBitsNonSRGB;
+ bitsToNumber = bitsToNumberNonSRGB;
+ }
+
+ let bitsToULPFromZero: ComponentMapFn;
+ if (opt.signed) {
+ const maxValue = (1 << (bitLength - 1)) - 1; // e.g. 127 for snorm8
+ bitsToULPFromZero = applyEach(
+ n => Math.max(-maxValue, signExtend(n, bitLength)),
+ componentOrder
+ );
+ } else {
+ bitsToULPFromZero = components => components;
+ }
+
+ const dataType: ComponentDataType = opt.signed ? 'snorm' : 'unorm';
+ return {
+ componentOrder,
+ componentInfo: makePerTexelComponent(componentOrder, {
+ dataType,
+ bitLength,
+ }),
+ encode,
+ decode,
+ pack: (components: PerTexelComponent<number>) =>
+ packComponents(componentOrder, components, bitLength, dataType),
+ unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
+ numberToBits,
+ bitsToNumber,
+ bitsToULPFromZero,
+ numericRange: { min: opt.signed ? -1 : 0, max: 1 },
+ };
+}
+
+/**
+ * Create an entry in `kTexelRepresentationInfo` for integer texel data with constant bitlength.
+ * @param {TexelComponent[]} componentOrder - The order of the component data.
+ * @param {number} bitLength - The number of bits in each component.
+ * @param {{signed: boolean}} opt - Boolean flag for `signed`.
+ */
+function makeIntegerInfo(
+ componentOrder: TexelComponent[],
+ bitLength: number,
+ opt: { signed: boolean }
+): TexelRepresentationInfo {
+ assert(bitLength <= 32);
+ const encode = applyEach(
+ (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n),
+ componentOrder
+ );
+ const decode = applyEach(
+ (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n),
+ componentOrder
+ );
+
+ let bitsToULPFromZero: ComponentMapFn;
+ if (opt.signed) {
+ bitsToULPFromZero = applyEach(n => signExtend(n, bitLength), componentOrder);
+ } else {
+ bitsToULPFromZero = components => components;
+ }
+
+ const dataType: ComponentDataType = opt.signed ? 'sint' : 'uint';
+ const bitMask = (1 << bitLength) - 1;
+ return {
+ componentOrder,
+ componentInfo: makePerTexelComponent(componentOrder, {
+ dataType,
+ bitLength,
+ }),
+ encode,
+ decode,
+ pack: (components: PerTexelComponent<number>) =>
+ packComponents(componentOrder, components, bitLength, dataType),
+ unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
+ numberToBits: applyEach(v => v & bitMask, componentOrder),
+ bitsToNumber: decode,
+ bitsToULPFromZero,
+ numericRange: opt.signed
+ ? { min: -(2 ** (bitLength - 1)), max: 2 ** (bitLength - 1) - 1 }
+ : { min: 0, max: 2 ** bitLength - 1 },
+ };
+}
+
+/**
+ * Create an entry in `kTexelRepresentationInfo` for floating point texel data with constant
+ * bitlength.
+ * @param {TexelComponent[]} componentOrder - The order of the component data.
+ * @param {number} bitLength - The number of bits in each component.
+ */
+function makeFloatInfo(
+ componentOrder: TexelComponent[],
+ bitLength: number,
+ { restrictedDepth = false }: { restrictedDepth?: boolean } = {}
+): TexelRepresentationInfo {
+ let encode: ComponentMapFn;
+ let numberToBits;
+ let bitsToNumber;
+ let bitsToULPFromZero;
+ switch (bitLength) {
+ case 32:
+ if (restrictedDepth) {
+ encode = applyEach(v => {
+ assert(v >= 0.0 && v <= 1.0, 'depth out of range');
+ return new Float32Array([v])[0];
+ }, componentOrder);
+ } else {
+ encode = applyEach(v => new Float32Array([v])[0], componentOrder);
+ }
+ numberToBits = applyEach(numberToFloat32Bits, componentOrder);
+ bitsToNumber = applyEach(float32BitsToNumber, componentOrder);
+ bitsToULPFromZero = applyEach(
+ v => floatBitsToNormalULPFromZero(v, kFloat32Format),
+ componentOrder
+ );
+ break;
+ case 16:
+ if (restrictedDepth) {
+ encode = applyEach(v => {
+ assert(v >= 0.0 && v <= 1.0, 'depth out of range');
+ return float16BitsToFloat32(float32ToFloat16Bits(v));
+ }, componentOrder);
+ } else {
+ encode = applyEach(v => float16BitsToFloat32(float32ToFloat16Bits(v)), componentOrder);
+ }
+ numberToBits = applyEach(float32ToFloat16Bits, componentOrder);
+ bitsToNumber = applyEach(float16BitsToFloat32, componentOrder);
+ bitsToULPFromZero = applyEach(
+ v => floatBitsToNormalULPFromZero(v, kFloat16Format),
+ componentOrder
+ );
+ break;
+ default:
+ unreachable();
+ }
+ const decode = applyEach(identity, componentOrder);
+
+ return {
+ componentOrder,
+ componentInfo: makePerTexelComponent(componentOrder, {
+ dataType: 'float' as const,
+ bitLength,
+ }),
+ encode,
+ decode,
+ pack: (components: PerTexelComponent<number>) => {
+ switch (bitLength) {
+ case 16:
+ components = applyEach(float32ToFloat16Bits, componentOrder)(components);
+ return packComponents(componentOrder, components, 16, 'uint');
+ case 32:
+ return packComponents(componentOrder, components, bitLength, 'float');
+ default:
+ unreachable();
+ }
+ },
+ unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
+ numberToBits,
+ bitsToNumber,
+ bitsToULPFromZero,
+ numericRange: restrictedDepth
+ ? { min: 0, max: 1 }
+ : { min: Number.NEGATIVE_INFINITY, max: Number.POSITIVE_INFINITY },
+ };
+}
+
+const kR = [TexelComponent.R];
+const kRG = [TexelComponent.R, TexelComponent.G];
+const kRGB = [TexelComponent.R, TexelComponent.G, TexelComponent.B];
+const kRGBA = [TexelComponent.R, TexelComponent.G, TexelComponent.B, TexelComponent.A];
+const kBGRA = [TexelComponent.B, TexelComponent.G, TexelComponent.R, TexelComponent.A];
+
+const identity = (n: number) => n;
+
+const kFloat11Format = { signed: 0, exponentBits: 5, mantissaBits: 6, bias: 15 } as const;
+const kFloat10Format = { signed: 0, exponentBits: 5, mantissaBits: 5, bias: 15 } as const;
+const kFloat9e5Format = { signed: 0, exponentBits: 5, mantissaBits: 9, bias: 15 } as const;
+
+export type TexelRepresentationInfo = {
+ /** Order of components in the packed representation. */
+ readonly componentOrder: TexelComponent[];
+ /** Data type and bit length of each component in the format. */
+ readonly componentInfo: PerTexelComponent<{
+ dataType: ComponentDataType;
+ bitLength: number;
+ }>;
+ /** Encode shader values into their data representation. ex.) float 1.0 -> unorm8 255 */
+ // MAINTENANCE_TODO: Replace with numberToBits?
+ readonly encode: ComponentMapFn;
+ /** Decode the data representation into the shader values. ex.) unorm8 255 -> float 1.0 */
+ // MAINTENANCE_TODO: Replace with bitsToNumber?
+ readonly decode: ComponentMapFn;
+ /** Pack texel component values into an ArrayBuffer. ex.) rg8unorm `{r: 0, g: 255}` -> 0xFF00 */
+ // MAINTENANCE_TODO: Replace with packBits?
+ readonly pack: ComponentPackFn;
+
+ /** Convert integer bit representations into numeric values, e.g. unorm8 255 -> numeric 1.0 */
+ readonly bitsToNumber: ComponentMapFn;
+ /** Convert numeric values into integer bit representations, e.g. numeric 1.0 -> unorm8 255 */
+ readonly numberToBits: ComponentMapFn;
+ /** Unpack integer bit representations from an ArrayBuffer, e.g. 0xFF00 -> rg8unorm [0,255] */
+ readonly unpackBits: ComponentUnpackFn;
+ /** Convert integer bit representations into ULPs-from-zero, e.g. unorm8 255 -> 255 ULPs */
+ readonly bitsToULPFromZero: ComponentMapFn;
+ /** The valid range of numeric "color" values, e.g. [0, Infinity] for ufloat. */
+ readonly numericRange: null | { min: number; max: number };
+
+ // Add fields as needed
+};
+export const kTexelRepresentationInfo: {
+ readonly [k in UncompressedTextureFormat]: TexelRepresentationInfo;
+} = {
+ .../* prettier-ignore */ {
+ 'r8unorm': makeNormalizedInfo( kR, 8, { signed: false, sRGB: false }),
+ 'r8snorm': makeNormalizedInfo( kR, 8, { signed: true, sRGB: false }),
+ 'r8uint': makeIntegerInfo( kR, 8, { signed: false }),
+ 'r8sint': makeIntegerInfo( kR, 8, { signed: true }),
+ 'r16uint': makeIntegerInfo( kR, 16, { signed: false }),
+ 'r16sint': makeIntegerInfo( kR, 16, { signed: true }),
+ 'r16float': makeFloatInfo( kR, 16),
+ 'rg8unorm': makeNormalizedInfo( kRG, 8, { signed: false, sRGB: false }),
+ 'rg8snorm': makeNormalizedInfo( kRG, 8, { signed: true, sRGB: false }),
+ 'rg8uint': makeIntegerInfo( kRG, 8, { signed: false }),
+ 'rg8sint': makeIntegerInfo( kRG, 8, { signed: true }),
+ 'r32uint': makeIntegerInfo( kR, 32, { signed: false }),
+ 'r32sint': makeIntegerInfo( kR, 32, { signed: true }),
+ 'r32float': makeFloatInfo( kR, 32),
+ 'rg16uint': makeIntegerInfo( kRG, 16, { signed: false }),
+ 'rg16sint': makeIntegerInfo( kRG, 16, { signed: true }),
+ 'rg16float': makeFloatInfo( kRG, 16),
+ 'rgba8unorm': makeNormalizedInfo(kRGBA, 8, { signed: false, sRGB: false }),
+ 'rgba8unorm-srgb': makeNormalizedInfo(kRGBA, 8, { signed: false, sRGB: true }),
+ 'rgba8snorm': makeNormalizedInfo(kRGBA, 8, { signed: true, sRGB: false }),
+ 'rgba8uint': makeIntegerInfo( kRGBA, 8, { signed: false }),
+ 'rgba8sint': makeIntegerInfo( kRGBA, 8, { signed: true }),
+ 'bgra8unorm': makeNormalizedInfo(kBGRA, 8, { signed: false, sRGB: false }),
+ 'bgra8unorm-srgb': makeNormalizedInfo(kBGRA, 8, { signed: false, sRGB: true }),
+ 'rg32uint': makeIntegerInfo( kRG, 32, { signed: false }),
+ 'rg32sint': makeIntegerInfo( kRG, 32, { signed: true }),
+ 'rg32float': makeFloatInfo( kRG, 32),
+ 'rgba16uint': makeIntegerInfo( kRGBA, 16, { signed: false }),
+ 'rgba16sint': makeIntegerInfo( kRGBA, 16, { signed: true }),
+ 'rgba16float': makeFloatInfo( kRGBA, 16),
+ 'rgba32uint': makeIntegerInfo( kRGBA, 32, { signed: false }),
+ 'rgba32sint': makeIntegerInfo( kRGBA, 32, { signed: true }),
+ 'rgba32float': makeFloatInfo( kRGBA, 32),
+ },
+ ...{
+ rgb10a2unorm: {
+ componentOrder: kRGBA,
+ componentInfo: {
+ R: { dataType: 'unorm', bitLength: 10 },
+ G: { dataType: 'unorm', bitLength: 10 },
+ B: { dataType: 'unorm', bitLength: 10 },
+ A: { dataType: 'unorm', bitLength: 2 },
+ },
+ encode: components => {
+ return {
+ R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false),
+ G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false),
+ B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false),
+ A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false),
+ };
+ },
+ decode: components => {
+ return {
+ R: normalizedIntegerAsFloat(components.R ?? unreachable(), 10, false),
+ G: normalizedIntegerAsFloat(components.G ?? unreachable(), 10, false),
+ B: normalizedIntegerAsFloat(components.B ?? unreachable(), 10, false),
+ A: normalizedIntegerAsFloat(components.A ?? unreachable(), 2, false),
+ };
+ },
+ pack: components =>
+ packComponents(
+ kRGBA,
+ components,
+ {
+ R: 10,
+ G: 10,
+ B: 10,
+ A: 2,
+ },
+ 'uint'
+ ),
+ unpackBits: (data: Uint8Array) =>
+ unpackComponentsBits(kRGBA, data, { R: 10, G: 10, B: 10, A: 2 }),
+ numberToBits: components => ({
+ R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false),
+ G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false),
+ B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false),
+ A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false),
+ }),
+ bitsToNumber: components => ({
+ R: normalizedIntegerAsFloat(components.R!, 10, false),
+ G: normalizedIntegerAsFloat(components.G!, 10, false),
+ B: normalizedIntegerAsFloat(components.B!, 10, false),
+ A: normalizedIntegerAsFloat(components.A!, 2, false),
+ }),
+ bitsToULPFromZero: components => components,
+ numericRange: { min: 0, max: 1 },
+ },
+ rg11b10ufloat: {
+ componentOrder: kRGB,
+ encode: applyEach(identity, kRGB),
+ decode: applyEach(identity, kRGB),
+ componentInfo: {
+ R: { dataType: 'ufloat', bitLength: 11 },
+ G: { dataType: 'ufloat', bitLength: 11 },
+ B: { dataType: 'ufloat', bitLength: 10 },
+ },
+ pack: components => {
+ const componentsBits = {
+ R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 6, 15),
+ G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 6, 15),
+ B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 5, 15),
+ };
+ return packComponents(
+ kRGB,
+ componentsBits,
+ {
+ R: 11,
+ G: 11,
+ B: 10,
+ },
+ 'uint'
+ );
+ },
+ unpackBits: (data: Uint8Array) => unpackComponentsBits(kRGB, data, { R: 11, G: 11, B: 10 }),
+ numberToBits: components => ({
+ R: numberToFloatBits(components.R ?? unreachable(), kFloat11Format),
+ G: numberToFloatBits(components.G ?? unreachable(), kFloat11Format),
+ B: numberToFloatBits(components.B ?? unreachable(), kFloat10Format),
+ }),
+ bitsToNumber: components => ({
+ R: floatBitsToNumber(components.R!, kFloat11Format),
+ G: floatBitsToNumber(components.G!, kFloat11Format),
+ B: floatBitsToNumber(components.B!, kFloat10Format),
+ }),
+ bitsToULPFromZero: components => ({
+ R: floatBitsToNormalULPFromZero(components.R!, kFloat11Format),
+ G: floatBitsToNormalULPFromZero(components.G!, kFloat11Format),
+ B: floatBitsToNormalULPFromZero(components.B!, kFloat10Format),
+ }),
+ numericRange: { min: 0, max: Number.POSITIVE_INFINITY },
+ },
+ rgb9e5ufloat: {
+ componentOrder: kRGB,
+ componentInfo: makePerTexelComponent(kRGB, {
+ dataType: 'ufloat',
+ bitLength: -1, // Components don't really have a bitLength since the format is packed.
+ }),
+ encode: applyEach(identity, kRGB),
+ decode: applyEach(identity, kRGB),
+ pack: components =>
+ new Uint32Array([
+ packRGB9E5UFloat(
+ components.R ?? unreachable(),
+ components.G ?? unreachable(),
+ components.B ?? unreachable()
+ ),
+ ]).buffer,
+ // For the purpose of unpacking, expand into three "ufloat14" values.
+ unpackBits: (data: Uint8Array) => {
+ // Pretend the exponent part is A so we can use unpackComponentsBits.
+ const parts = unpackComponentsBits(kRGBA, data, { R: 9, G: 9, B: 9, A: 5 });
+ return {
+ R: (parts.A! << 9) | parts.R!,
+ G: (parts.A! << 9) | parts.G!,
+ B: (parts.A! << 9) | parts.B!,
+ };
+ },
+ numberToBits: components => ({
+ R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 9, 15),
+ G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 9, 15),
+ B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 9, 15),
+ }),
+ bitsToNumber: components => ({
+ R: floatBitsToNumber(components.R!, kFloat9e5Format),
+ G: floatBitsToNumber(components.G!, kFloat9e5Format),
+ B: floatBitsToNumber(components.B!, kFloat9e5Format),
+ }),
+ bitsToULPFromZero: components => ({
+ R: floatBitsToNormalULPFromZero(components.R!, kFloat9e5Format),
+ G: floatBitsToNormalULPFromZero(components.G!, kFloat9e5Format),
+ B: floatBitsToNormalULPFromZero(components.B!, kFloat9e5Format),
+ }),
+ numericRange: { min: 0, max: Number.POSITIVE_INFINITY },
+ },
+ depth32float: makeFloatInfo([TexelComponent.Depth], 32, { restrictedDepth: true }),
+ depth16unorm: makeNormalizedInfo([TexelComponent.Depth], 16, { signed: false, sRGB: false }),
+ depth24plus: {
+ componentOrder: [TexelComponent.Depth],
+ componentInfo: { Depth: { dataType: null, bitLength: 24 } },
+ encode: applyEach(() => unreachable('depth24plus cannot be encoded'), [TexelComponent.Depth]),
+ decode: applyEach(() => unreachable('depth24plus cannot be decoded'), [TexelComponent.Depth]),
+ pack: () => unreachable('depth24plus data cannot be packed'),
+ unpackBits: () => unreachable('depth24plus data cannot be unpacked'),
+ numberToBits: () => unreachable('depth24plus has no representation'),
+ bitsToNumber: () => unreachable('depth24plus has no representation'),
+ bitsToULPFromZero: () => unreachable('depth24plus has no representation'),
+ numericRange: { min: 0, max: 1 },
+ },
+ stencil8: makeIntegerInfo([TexelComponent.Stencil], 8, { signed: false }),
+ 'depth32float-stencil8': {
+ componentOrder: [TexelComponent.Depth, TexelComponent.Stencil],
+ componentInfo: {
+ Depth: {
+ dataType: 'float',
+ bitLength: 32,
+ },
+ Stencil: {
+ dataType: 'uint',
+ bitLength: 8,
+ },
+ },
+ encode: components => {
+ assert(components.Stencil !== undefined);
+ assertInIntegerRange(components.Stencil, 8, false);
+ return components;
+ },
+ decode: components => {
+ assert(components.Stencil !== undefined);
+ assertInIntegerRange(components.Stencil, 8, false);
+ return components;
+ },
+ pack: () => unreachable('depth32float-stencil8 data cannot be packed'),
+ unpackBits: () => unreachable('depth32float-stencil8 data cannot be unpacked'),
+ numberToBits: () => unreachable('not implemented'),
+ bitsToNumber: () => unreachable('not implemented'),
+ bitsToULPFromZero: () => unreachable('not implemented'),
+ numericRange: null,
+ },
+ 'depth24plus-stencil8': {
+ componentOrder: [TexelComponent.Depth, TexelComponent.Stencil],
+ componentInfo: {
+ Depth: {
+ dataType: null,
+ bitLength: 24,
+ },
+ Stencil: {
+ dataType: 'uint',
+ bitLength: 8,
+ },
+ },
+ encode: components => {
+ assert(components.Depth === undefined, 'depth24plus cannot be encoded');
+ assert(components.Stencil !== undefined);
+ assertInIntegerRange(components.Stencil, 8, false);
+ return components;
+ },
+ decode: components => {
+ assert(components.Depth === undefined, 'depth24plus cannot be decoded');
+ assert(components.Stencil !== undefined);
+ assertInIntegerRange(components.Stencil, 8, false);
+ return components;
+ },
+ pack: () => unreachable('depth24plus-stencil8 data cannot be packed'),
+ unpackBits: () => unreachable('depth24plus-stencil8 data cannot be unpacked'),
+ numberToBits: () => unreachable('depth24plus-stencil8 has no representation'),
+ bitsToNumber: () => unreachable('depth24plus-stencil8 has no representation'),
+ bitsToULPFromZero: () => unreachable('depth24plus-stencil8 has no representation'),
+ numericRange: null,
+ },
+ },
+};
+
+/**
+ * Get the `ComponentDataType` for a format. All components must have the same type.
+ * @param {UncompressedTextureFormat} format - The input format.
+ * @returns {ComponentDataType} The data of the components.
+ */
+export function getSingleDataType(format: UncompressedTextureFormat): ComponentDataType {
+ const infos = Object.values(kTexelRepresentationInfo[format].componentInfo);
+ assert(infos.length > 0);
+ return infos.reduce((acc, cur) => {
+ assert(cur !== undefined);
+ assert(acc === undefined || acc === cur.dataType);
+ return cur.dataType;
+ }, infos[0]!.dataType);
+}
+
+/**
+ * Get traits for generating code to readback data from a component.
+ * @param {ComponentDataType} dataType - The input component data type.
+ * @returns A dictionary containing the respective `ReadbackTypedArray` and `shaderType`.
+ */
+export function getComponentReadbackTraits(dataType: ComponentDataType) {
+ switch (dataType) {
+ case 'ufloat':
+ case 'float':
+ case 'unorm':
+ case 'snorm':
+ return {
+ ReadbackTypedArray: Float32Array,
+ shaderType: 'f32' as const,
+ };
+ case 'uint':
+ return {
+ ReadbackTypedArray: Uint32Array,
+ shaderType: 'u32' as const,
+ };
+ case 'sint':
+ return {
+ ReadbackTypedArray: Int32Array,
+ shaderType: 'i32' as const,
+ };
+ default:
+ unreachable();
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts
new file mode 100644
index 0000000000..33c27efbbc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texel_view.ts
@@ -0,0 +1,160 @@
+import { assert, memcpy } from '../../../common/util/util.js';
+import { EncodableTextureFormat, kTextureFormatInfo } from '../../capability_info.js';
+import { reifyExtent3D, reifyOrigin3D } from '../unions.js';
+
+import { kTexelRepresentationInfo, makeClampToRange, PerTexelComponent } from './texel_data.js';
+
+/** Function taking some x,y,z coordinates and returning `Readonly<T>`. */
+export type PerPixelAtLevel<T> = (coords: Required<GPUOrigin3DDict>) => Readonly<T>;
+
+/**
+ * Wrapper to view various representations of texture data in other ways. E.g., can:
+ * - Provide a mapped buffer, containing copied texture data, and read color values.
+ * - Provide a function that generates color values by coordinate, and convert to ULPs-from-zero.
+ *
+ * MAINTENANCE_TODO: Would need some refactoring to support block formats, which could be partially
+ * supported if useful.
+ */
+export class TexelView {
+ /** The GPUTextureFormat of the TexelView. */
+ readonly format: EncodableTextureFormat;
+ /** Generates the bytes for the texel at the given coordinates. */
+ readonly bytes: PerPixelAtLevel<Uint8Array>;
+ /** Generates the ULPs-from-zero for the texel at the given coordinates. */
+ readonly ulpFromZero: PerPixelAtLevel<PerTexelComponent<number>>;
+ /** Generates the color for the texel at the given coordinates. */
+ readonly color: PerPixelAtLevel<PerTexelComponent<number>>;
+
+ private constructor(
+ format: EncodableTextureFormat,
+ {
+ bytes,
+ ulpFromZero,
+ color,
+ }: {
+ bytes: PerPixelAtLevel<Uint8Array>;
+ ulpFromZero: PerPixelAtLevel<PerTexelComponent<number>>;
+ color: PerPixelAtLevel<PerTexelComponent<number>>;
+ }
+ ) {
+ this.format = format;
+ this.bytes = bytes;
+ this.ulpFromZero = ulpFromZero;
+ this.color = color;
+ }
+
+ /**
+ * Produces a TexelView from "linear image data", i.e. the `writeTexture` format. Takes a
+ * reference to the input `subrectData`, so any changes to it will be visible in the TexelView.
+ */
+ static fromTextureDataByReference(
+ format: EncodableTextureFormat,
+ subrectData: Uint8Array | Uint8ClampedArray,
+ {
+ bytesPerRow,
+ rowsPerImage,
+ subrectOrigin,
+ subrectSize,
+ }: {
+ bytesPerRow: number;
+ rowsPerImage: number;
+ subrectOrigin: GPUOrigin3D;
+ subrectSize: GPUExtent3D;
+ }
+ ) {
+ const origin = reifyOrigin3D(subrectOrigin);
+ const size = reifyExtent3D(subrectSize);
+
+ const info = kTextureFormatInfo[format];
+ assert(info.blockWidth === 1 && info.blockHeight === 1, 'unimplemented for block formats');
+
+ return TexelView.fromTexelsAsBytes(format, coords => {
+ assert(
+ coords.x >= origin.x &&
+ coords.y >= origin.y &&
+ coords.z >= origin.z &&
+ coords.x < origin.x + size.width &&
+ coords.y < origin.y + size.height &&
+ coords.z < origin.z + size.depthOrArrayLayers,
+ 'coordinate out of bounds'
+ );
+
+ const imageOffsetInRows = (coords.z - origin.z) * rowsPerImage;
+ const rowOffset = (imageOffsetInRows + (coords.y - origin.y)) * bytesPerRow;
+ const offset = rowOffset + (coords.x - origin.x) * info.bytesPerBlock;
+
+ // MAINTENANCE_TODO: To support block formats, decode the block and then index into the result.
+ return subrectData.subarray(offset, offset + info.bytesPerBlock) as Uint8Array;
+ });
+ }
+
+ /** Produces a TexelView from a generator of bytes for individual texel blocks. */
+ static fromTexelsAsBytes(
+ format: EncodableTextureFormat,
+ generator: PerPixelAtLevel<Uint8Array>
+ ): TexelView {
+ const info = kTextureFormatInfo[format];
+ assert(info.blockWidth === 1 && info.blockHeight === 1, 'unimplemented for block formats');
+
+ const repr = kTexelRepresentationInfo[format];
+ return new TexelView(format, {
+ bytes: generator,
+ ulpFromZero: coords => repr.bitsToULPFromZero(repr.unpackBits(generator(coords))),
+ color: coords => repr.bitsToNumber(repr.unpackBits(generator(coords))),
+ });
+ }
+
+ /** Produces a TexelView from a generator of numeric "color" values for each texel. */
+ static fromTexelsAsColors(
+ format: EncodableTextureFormat,
+ generator: PerPixelAtLevel<PerTexelComponent<number>>,
+ { clampToFormatRange = false }: { clampToFormatRange?: boolean } = {}
+ ): TexelView {
+ const info = kTextureFormatInfo[format];
+ assert(info.blockWidth === 1 && info.blockHeight === 1, 'unimplemented for block formats');
+
+ if (clampToFormatRange) {
+ const applyClamp = makeClampToRange(format);
+ const oldGenerator = generator;
+ generator = coords => applyClamp(oldGenerator(coords));
+ }
+
+ const repr = kTexelRepresentationInfo[format];
+ return new TexelView(format, {
+ bytes: coords => new Uint8Array(repr.pack(repr.encode(generator(coords)))),
+ ulpFromZero: coords => repr.bitsToULPFromZero(repr.numberToBits(generator(coords))),
+ color: generator,
+ });
+ }
+
+ /** Writes the contents of a TexelView as "linear image data", i.e. the `writeTexture` format. */
+ writeTextureData(
+ subrectData: Uint8Array | Uint8ClampedArray,
+ {
+ bytesPerRow,
+ rowsPerImage,
+ subrectOrigin: subrectOrigin_,
+ subrectSize: subrectSize_,
+ }: {
+ bytesPerRow: number;
+ rowsPerImage: number;
+ subrectOrigin: GPUOrigin3D;
+ subrectSize: GPUExtent3D;
+ }
+ ): void {
+ const subrectOrigin = reifyOrigin3D(subrectOrigin_);
+ const subrectSize = reifyExtent3D(subrectSize_);
+
+ const info = kTextureFormatInfo[this.format];
+ assert(info.blockWidth === 1 && info.blockHeight === 1, 'unimplemented for block formats');
+
+ for (let z = subrectOrigin.z; z < subrectOrigin.z + subrectSize.depthOrArrayLayers; ++z) {
+ for (let y = subrectOrigin.y; y < subrectOrigin.y + subrectSize.height; ++y) {
+ for (let x = subrectOrigin.x; x < subrectOrigin.x + subrectSize.width; ++x) {
+ const start = (z * rowsPerImage + y) * bytesPerRow + x * info.bytesPerBlock;
+ memcpy({ src: this.bytes({ x, y, z }) }, { dst: subrectData, start });
+ }
+ }
+ }
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.spec.ts
new file mode 100644
index 0000000000..a3ebcb1f4c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.spec.ts
@@ -0,0 +1,159 @@
+export const description = 'checkPixels helpers behave as expected against real textures';
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { GPUTest } from '../../gpu_test.js';
+
+import { TexelView } from './texel_view.js';
+import { textureContentIsOKByT2B } from './texture_ok.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('float32')
+ .desc(`Basic test that actual/expected must match, for float32.`)
+ .params(u =>
+ u
+ .combineWithParams([
+ { format: 'rgba32float' }, //
+ { format: 'rg32float' },
+ ] as const)
+ .beginSubcases()
+ .combineWithParams([
+ // Expected data is 0.6 in all channels
+ { data: 0.6, opts: { maxFractionalDiff: 0.0000001 }, _ok: true },
+ { data: 0.6, opts: { maxDiffULPsForFloatFormat: 1 }, _ok: true },
+
+ { data: 0.5999, opts: { maxFractionalDiff: 0 }, _ok: false },
+ { data: 0.5999, opts: { maxFractionalDiff: 0.0001001 }, _ok: true },
+
+ { data: 0.6001, opts: { maxFractionalDiff: 0 }, _ok: false },
+ { data: 0.6001, opts: { maxFractionalDiff: 0.0001001 }, _ok: true },
+
+ { data: 0.5999, opts: { maxDiffULPsForFloatFormat: 1677 }, _ok: false },
+ { data: 0.5999, opts: { maxDiffULPsForFloatFormat: 1678 }, _ok: true },
+
+ { data: 0.6001, opts: { maxDiffULPsForFloatFormat: 1676 }, _ok: false },
+ { data: 0.6001, opts: { maxDiffULPsForFloatFormat: 1677 }, _ok: true },
+ ])
+ )
+ .fn(async t => {
+ const { format, data, opts, _ok } = t.params;
+
+ const size = [1, 1];
+ const texture = t.device.createTexture({
+ format,
+ size,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ });
+ t.trackForCleanup(texture);
+ t.device.queue.writeTexture({ texture }, new Float32Array([data, data, data, data]), {}, size);
+
+ const expColor = { R: 0.6, G: 0.6, B: 0.6, A: 0.6 };
+ const expTexelView = TexelView.fromTexelsAsColors(format, coords => expColor);
+
+ const result = await textureContentIsOKByT2B(t, { texture }, size, { expTexelView }, opts);
+ t.expect((result === undefined) === _ok, `expected ${_ok}, got ${result === undefined}`);
+ });
+
+g.test('norm')
+ .desc(`Basic test that actual/expected must match, for unorm/snorm.`)
+ .params(u =>
+ u
+ .combine('mode', ['bytes', 'colors'] as const)
+ .combineWithParams([
+ { format: 'r8unorm', _maxValue: 255 },
+ { format: 'r8snorm', _maxValue: 127 },
+ ] as const)
+ .beginSubcases()
+ .combineWithParams([
+ // Expected data is [10, 10]
+ { data: [10, 10], _ok: true },
+ { data: [10, 11], _ok: false },
+ { data: [11, 10], _ok: false },
+ { data: [11, 11], _ok: false },
+ ])
+ )
+ .fn(async t => {
+ const { mode, format, _maxValue, data, _ok } = t.params;
+
+ const size = [2, 1];
+ const texture = t.device.createTexture({
+ format,
+ size,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ });
+ t.trackForCleanup(texture);
+ t.device.queue.writeTexture({ texture }, new Int8Array(data), {}, size);
+
+ let expTexelView;
+ switch (mode) {
+ case 'bytes':
+ expTexelView = TexelView.fromTexelsAsBytes(format, coords => new Uint8Array([10]));
+ break;
+ case 'colors':
+ expTexelView = TexelView.fromTexelsAsColors(format, coords => ({ R: 10 / _maxValue }));
+ break;
+ }
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture },
+ size,
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 0 }
+ );
+ t.expect((result === undefined) === _ok, result?.message);
+ });
+
+g.test('snorm_min')
+ .desc(
+ `The minimum snorm value has two possible representations (e.g. -127 and -128). Ensure that
+ actual/expected can mismatch in both directions and pass the test.`
+ )
+ .params(u =>
+ u //
+ .combine('mode', ['bytes', 'colors'] as const)
+ .combineWithParams([
+ //
+ { format: 'r8snorm', _maxValue: 127 },
+ ] as const)
+ )
+ .fn(async t => {
+ const { mode, format, _maxValue } = t.params;
+
+ const data = [-_maxValue, -_maxValue - 1];
+
+ const size = [2, 1];
+ const texture = t.device.createTexture({
+ format,
+ size,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ });
+ t.trackForCleanup(texture);
+ t.device.queue.writeTexture({ texture }, new Int8Array(data), {}, size);
+
+ let expTexelView;
+ switch (mode) {
+ case 'bytes':
+ {
+ // Actual value should be [-127,-128], expected value is [-128,-127], both should pass.
+ const exp = [-_maxValue - 1, -_maxValue];
+ expTexelView = TexelView.fromTexelsAsBytes(
+ format,
+ coords => new Uint8Array([exp[coords.x]])
+ );
+ }
+ break;
+ case 'colors':
+ expTexelView = TexelView.fromTexelsAsColors(format, coords => ({ R: -1 }));
+ break;
+ }
+
+ const result = await textureContentIsOKByT2B(
+ t,
+ { texture },
+ size,
+ { expTexelView },
+ { maxDiffULPsForNormFormat: 0 }
+ );
+ t.expectOK(result);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts
new file mode 100644
index 0000000000..3481c90b1f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts
@@ -0,0 +1,341 @@
+import { assert, ErrorWithExtra, unreachable } from '../../../common/util/util.js';
+import { EncodableTextureFormat, kTextureFormatInfo } from '../../capability_info.js';
+import { GPUTest } from '../../gpu_test.js';
+import { generatePrettyTable } from '../pretty_diff_tables.js';
+import { reifyExtent3D, reifyOrigin3D } from '../unions.js';
+
+import { getTextureSubCopyLayout } from './layout.js';
+import { kTexelRepresentationInfo, PerTexelComponent, TexelComponent } from './texel_data.js';
+import { TexelView } from './texel_view.js';
+
+type PerPixelAtLevel<T> = (coords: Required<GPUOrigin3DDict>) => T;
+
+/** Threshold options for comparing texels of different formats (norm/float/int). */
+export type TexelCompareOptions = {
+ /** Threshold for integer texture formats. Defaults to 0. */
+ maxIntDiff?: number;
+ /** Threshold for non-integer (norm/float) texture formats, if not overridden. */
+ maxFractionalDiff?: number;
+ /** Threshold in ULPs for unorm/snorm texture formats. Overrides `maxFractionalDiff`. */
+ maxDiffULPsForNormFormat?: number;
+ /** Threshold in ULPs for float/ufloat texture formats. Overrides `maxFractionalDiff`. */
+ maxDiffULPsForFloatFormat?: number;
+};
+
+type TexelViewComparer = {
+ /** Given coords, returns whether the two texel views are considered matching at that point. */
+ predicate: PerPixelAtLevel<boolean>;
+ /**
+ * Given a list of failed coords, returns table rows for `generatePrettyTable` that
+ * display the actual/expected values and diffs for debugging.
+ */
+ tableRows: (failedCoords: readonly Required<GPUOrigin3DDict>[]) => Iterable<string>[];
+};
+
+function makeTexelViewComparer(
+ format: EncodableTextureFormat,
+ { actTexelView, expTexelView }: { actTexelView: TexelView; expTexelView: TexelView },
+ opts: TexelCompareOptions
+): TexelViewComparer {
+ const {
+ maxIntDiff = 0,
+ maxFractionalDiff,
+ maxDiffULPsForNormFormat,
+ maxDiffULPsForFloatFormat,
+ } = opts;
+
+ assert(maxIntDiff >= 0, 'threshold must be non-negative');
+ if (maxFractionalDiff !== undefined) {
+ assert(maxFractionalDiff >= 0, 'threshold must be non-negative');
+ }
+ if (maxDiffULPsForFloatFormat !== undefined) {
+ assert(maxDiffULPsForFloatFormat >= 0, 'threshold must be non-negative');
+ }
+ if (maxDiffULPsForNormFormat !== undefined) {
+ assert(maxDiffULPsForNormFormat >= 0, 'threshold must be non-negative');
+ }
+
+ const fmtIsInt = format.includes('int');
+ const fmtIsNorm = format.includes('norm');
+ const fmtIsFloat = format.includes('float');
+
+ const tvc = {} as TexelViewComparer;
+ if (fmtIsInt) {
+ tvc.predicate = coords =>
+ comparePerComponent(actTexelView.color(coords), expTexelView.color(coords), maxIntDiff);
+ } else if (fmtIsNorm && maxDiffULPsForNormFormat !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.ulpFromZero(coords),
+ expTexelView.ulpFromZero(coords),
+ maxDiffULPsForNormFormat
+ );
+ } else if (fmtIsFloat && maxDiffULPsForFloatFormat !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.ulpFromZero(coords),
+ expTexelView.ulpFromZero(coords),
+ maxDiffULPsForFloatFormat
+ );
+ } else if (maxFractionalDiff !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.color(coords),
+ expTexelView.color(coords),
+ maxFractionalDiff
+ );
+ } else {
+ if (fmtIsNorm) {
+ unreachable('need maxFractionalDiff or maxDiffULPsForNormFormat to compare norm textures');
+ } else if (fmtIsFloat) {
+ unreachable('need maxFractionalDiff or maxDiffULPsForFloatFormat to compare float textures');
+ } else {
+ unreachable();
+ }
+ }
+
+ const repr = kTexelRepresentationInfo[format];
+ if (fmtIsInt) {
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${maxIntDiff}`],
+ (function* () {
+ yield* [` diff (act - exp)`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.color(coords);
+ const exp = expTexelView.color(coords);
+ yield repr.componentOrder.map(ch => act[ch]! - exp[ch]!).join(',');
+ }
+ })(),
+ ];
+ } else if (
+ (fmtIsNorm && maxDiffULPsForNormFormat !== undefined) ||
+ (fmtIsFloat && maxDiffULPsForFloatFormat !== undefined)
+ ) {
+ const toleranceULPs = fmtIsNorm ? maxDiffULPsForNormFormat! : maxDiffULPsForFloatFormat!;
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${toleranceULPs} normal-ULPs`],
+ (function* () {
+ yield* [` diff (act - exp) in normal-ULPs`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.ulpFromZero(coords);
+ const exp = expTexelView.ulpFromZero(coords);
+ yield repr.componentOrder.map(ch => act[ch]! - exp[ch]!).join(',');
+ }
+ })(),
+ ];
+ } else {
+ assert(maxFractionalDiff !== undefined);
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${maxFractionalDiff}`],
+ (function* () {
+ yield* [` diff (act - exp)`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.color(coords);
+ const exp = expTexelView.color(coords);
+ yield repr.componentOrder.map(ch => (act[ch]! - exp[ch]!).toPrecision(4)).join(',');
+ }
+ })(),
+ ];
+ }
+
+ return tvc;
+}
+
+function comparePerComponent(
+ actual: PerTexelComponent<number>,
+ expected: PerTexelComponent<number>,
+ maxDiff: number
+) {
+ return Object.keys(actual).every(key => {
+ const k = key as TexelComponent;
+ const act = actual[k]!;
+ const exp = expected[k];
+ if (exp === undefined) return false;
+ return Math.abs(act - exp) <= maxDiff;
+ });
+}
+
+/** Create a new mappable GPUBuffer, and copy a subrectangle of GPUTexture data into it. */
+function createTextureCopyForMapRead(
+ t: GPUTest,
+ source: GPUImageCopyTexture,
+ copySize: GPUExtent3D,
+ { format }: { format: EncodableTextureFormat }
+): { buffer: GPUBuffer; bytesPerRow: number; rowsPerImage: number } {
+ const { byteLength, bytesPerRow, rowsPerImage } = getTextureSubCopyLayout(format, copySize, {
+ aspect: source.aspect,
+ });
+
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ size: byteLength,
+ });
+ t.trackForCleanup(buffer);
+
+ const cmd = t.device.createCommandEncoder();
+ cmd.copyTextureToBuffer(source, { buffer, bytesPerRow, rowsPerImage }, copySize);
+ t.device.queue.submit([cmd.finish()]);
+
+ return { buffer, bytesPerRow, rowsPerImage };
+}
+
+function findFailedPixels(
+ format: EncodableTextureFormat,
+ subrectOrigin: Required<GPUOrigin3DDict>,
+ subrectSize: Required<GPUExtent3DDict>,
+ { actTexelView, expTexelView }: { actTexelView: TexelView; expTexelView: TexelView },
+ texelCompareOptions: TexelCompareOptions
+) {
+ const comparer = makeTexelViewComparer(
+ format,
+ { actTexelView, expTexelView },
+ texelCompareOptions
+ );
+
+ const lowerCorner = [subrectSize.width, subrectSize.height, subrectSize.depthOrArrayLayers];
+ const upperCorner = [0, 0, 0];
+ const failedPixels: Required<GPUOrigin3DDict>[] = [];
+ for (let z = subrectOrigin.z; z < subrectOrigin.z + subrectSize.depthOrArrayLayers; ++z) {
+ for (let y = subrectOrigin.y; y < subrectOrigin.y + subrectSize.height; ++y) {
+ for (let x = subrectOrigin.x; x < subrectOrigin.x + subrectSize.width; ++x) {
+ const coords = { x, y, z };
+
+ if (!comparer.predicate(coords)) {
+ failedPixels.push(coords);
+ lowerCorner[0] = Math.min(lowerCorner[0], x);
+ lowerCorner[1] = Math.min(lowerCorner[1], y);
+ lowerCorner[2] = Math.min(lowerCorner[2], z);
+ upperCorner[0] = Math.max(upperCorner[0], x);
+ upperCorner[1] = Math.max(upperCorner[1], y);
+ upperCorner[2] = Math.max(upperCorner[2], z);
+ }
+ }
+ }
+ }
+ if (failedPixels.length === 0) {
+ return undefined;
+ }
+
+ const info = kTextureFormatInfo[format];
+ const repr = kTexelRepresentationInfo[format];
+
+ const integerSampleType = info.sampleType === 'uint' || info.sampleType === 'sint';
+ const numberToString = integerSampleType
+ ? (n: number) => n.toFixed()
+ : (n: number) => n.toPrecision(6);
+
+ const componentOrderStr = repr.componentOrder.join(',') + ':';
+
+ const printCoords = (function* () {
+ yield* [' coords', '==', 'X,Y,Z:'];
+ for (const coords of failedPixels) yield `${coords.x},${coords.y},${coords.z}`;
+ })();
+ const printActualBytes = (function* () {
+ yield* [' act. texel bytes (little-endian)', '==', '0x:'];
+ for (const coords of failedPixels) {
+ yield Array.from(actTexelView.bytes(coords), b => b.toString(16).padStart(2, '0')).join(' ');
+ }
+ })();
+ const printActualColors = (function* () {
+ yield* [' act. colors', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = actTexelView.color(coords);
+ yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`;
+ }
+ })();
+ const printExpectedColors = (function* () {
+ yield* [' exp. colors', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = expTexelView.color(coords);
+ yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`;
+ }
+ })();
+ const printActualULPs = (function* () {
+ yield* [' act. normal-ULPs-from-zero', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = actTexelView.ulpFromZero(coords);
+ yield `${repr.componentOrder.map(ch => pixel[ch]).join(',')}`;
+ }
+ })();
+ const printExpectedULPs = (function* () {
+ yield* [` exp. normal-ULPs-from-zero`, '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = expTexelView.ulpFromZero(coords);
+ yield `${repr.componentOrder.map(ch => pixel[ch]).join(',')}`;
+ }
+ })();
+
+ const opts = {
+ fillToWidth: 120,
+ numberToString,
+ };
+ return `\
+ between ${lowerCorner} and ${upperCorner} inclusive:
+${generatePrettyTable(opts, [
+ printCoords,
+ printActualBytes,
+ printActualColors,
+ printExpectedColors,
+ printActualULPs,
+ printExpectedULPs,
+ ...comparer.tableRows(failedPixels),
+])}`;
+}
+
+/**
+ * Check the contents of a GPUTexture by reading it back (with copyTextureToBuffer+mapAsync), then
+ * comparing the data with the data in `expTexelView`.
+ *
+ * The actual and expected texture data are both converted to the "NormalULPFromZero" format,
+ * which is a signed number representing how far the number is from zero, in ULPs, skipping
+ * subnormal numbers (where ULP is defined for float, normalized, and integer formats).
+ */
+export async function textureContentIsOKByT2B(
+ t: GPUTest,
+ source: GPUImageCopyTexture,
+ copySize_: GPUExtent3D,
+ { expTexelView }: { expTexelView: TexelView },
+ texelCompareOptions: TexelCompareOptions
+): Promise<ErrorWithExtra | undefined> {
+ const subrectOrigin = reifyOrigin3D(source.origin ?? [0, 0, 0]);
+ const subrectSize = reifyExtent3D(copySize_);
+ const format = expTexelView.format;
+
+ const { buffer, bytesPerRow, rowsPerImage } = createTextureCopyForMapRead(
+ t,
+ source,
+ subrectSize,
+ { format }
+ );
+
+ await buffer.mapAsync(GPUMapMode.READ);
+ const data = new Uint8Array(buffer.getMappedRange());
+
+ const texelViewConfig = {
+ bytesPerRow,
+ rowsPerImage,
+ subrectOrigin,
+ subrectSize,
+ } as const;
+
+ const actTexelView = TexelView.fromTextureDataByReference(format, data, texelViewConfig);
+
+ const failedPixelsMessage = findFailedPixels(
+ format,
+ subrectOrigin,
+ subrectSize,
+ { actTexelView, expTexelView },
+ texelCompareOptions
+ );
+
+ if (failedPixelsMessage === undefined) {
+ return undefined;
+ }
+
+ const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage;
+ return new ErrorWithExtra(msg, () => ({
+ expTexelView,
+ // Make a new TexelView with a copy of the data so we can unmap the buffer (debug mode only).
+ actTexelView: TexelView.fromTextureDataByReference(format, data.slice(), texelViewConfig),
+ }));
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/unions.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/unions.ts
new file mode 100644
index 0000000000..23381ddee9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/unions.ts
@@ -0,0 +1,45 @@
+/**
+ * Reifies a `GPUOrigin3D` into a `Required<GPUOrigin3DDict>`.
+ */
+export function reifyOrigin3D(
+ val: Readonly<GPUOrigin3DDict> | Iterable<number>
+): Required<GPUOrigin3DDict> {
+ if (Symbol.iterator in val) {
+ const v = Array.from(val as Iterable<number>);
+ return {
+ x: v[0] ?? 0,
+ y: v[1] ?? 0,
+ z: v[2] ?? 0,
+ };
+ } else {
+ const v = val as Readonly<GPUOrigin3DDict>;
+ return {
+ x: v.x ?? 0,
+ y: v.y ?? 0,
+ z: v.z ?? 0,
+ };
+ }
+}
+
+/**
+ * Reifies a `GPUExtent3D` into a `Required<GPUExtent3DDict>`.
+ */
+export function reifyExtent3D(
+ val: Readonly<GPUExtent3DDict> | Iterable<number>
+): Required<GPUExtent3DDict> {
+ if (Symbol.iterator in val) {
+ const v = Array.from(val as Iterable<number>);
+ return {
+ width: v[0] ?? 1,
+ height: v[1] ?? 1,
+ depthOrArrayLayers: v[2] ?? 1,
+ };
+ } else {
+ const v = val as Readonly<GPUExtent3DDict>;
+ return {
+ width: v.width ?? 1,
+ height: v.height ?? 1,
+ depthOrArrayLayers: v.depthOrArrayLayers ?? 1,
+ };
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/README.txt
new file mode 100644
index 0000000000..802f5b17a2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/README.txt
@@ -0,0 +1,5 @@
+Tests for Web platform-specific interactions like GPUCanvasContext and canvas, WebXR,
+ImageBitmaps, and video APIs.
+
+TODO(#922): Also hopefully tests for user-initiated readbacks from WebGPU canvases
+(printing, save image as, etc.)
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/README.txt
new file mode 100644
index 0000000000..83194d5b11
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/README.txt
@@ -0,0 +1 @@
+Tests for WebGPU <canvas> and OffscreenCanvas presentation.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts
new file mode 100644
index 0000000000..3a29f55e72
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/configure.spec.ts
@@ -0,0 +1,424 @@
+export const description = `
+Tests for GPUCanvasContext.configure.
+
+TODO:
+- Test colorSpace
+- Test viewFormats
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+import {
+ kAllTextureFormats,
+ kCanvasTextureFormats,
+ kTextureUsages,
+ filterFormatsByFeature,
+ kFeaturesForFormats,
+ kTextureFormats,
+ viewCompatible,
+} from '../../capability_info.js';
+import { GPUConst } from '../../constants.js';
+import { GPUTest } from '../../gpu_test.js';
+import { kAllCanvasTypes, createCanvas } from '../../util/create_elements.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('defaults')
+ .desc(
+ `
+ Ensure that the defaults for GPUCanvasConfiguration are correct.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const { canvasType } = t.params;
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ });
+
+ const currentTexture = ctx.getCurrentTexture();
+ t.expect(currentTexture.format === 'rgba8unorm');
+ t.expect(currentTexture.usage === GPUTextureUsage.RENDER_ATTACHMENT);
+ t.expect(currentTexture.dimension === '2d');
+ t.expect(currentTexture.width === canvas.width);
+ t.expect(currentTexture.height === canvas.height);
+ t.expect(currentTexture.depthOrArrayLayers === 1);
+ t.expect(currentTexture.mipLevelCount === 1);
+ t.expect(currentTexture.sampleCount === 1);
+ });
+
+g.test('device')
+ .desc(
+ `
+ Ensure that configure reacts appropriately to various device states.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const { canvasType } = t.params;
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ // Calling configure without a device should throw.
+ t.shouldThrow(true, () => {
+ ctx.configure({
+ format: 'rgba8unorm',
+ } as GPUCanvasConfiguration);
+ });
+
+ // Device is not configured, so getCurrentTexture will throw.
+ t.shouldThrow(true, () => {
+ ctx.getCurrentTexture();
+ });
+
+ // Calling configure with a device should succeed.
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ });
+
+ // getCurrentTexture will succeed with a valid device.
+ ctx.getCurrentTexture();
+
+ // Unconfiguring should cause the device to be cleared.
+ ctx.unconfigure();
+ t.shouldThrow(true, () => {
+ ctx.getCurrentTexture();
+ });
+
+ // Should be able to successfully configure again after unconfiguring.
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ });
+ ctx.getCurrentTexture();
+ });
+
+g.test('format')
+ .desc(
+ `
+ Ensure that only valid texture formats are allowed when calling configure.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('format', kAllTextureFormats)
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { canvasType, format } = t.params;
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ // Would prefer to use kCanvasTextureFormats.includes(format), but that's giving TS errors.
+ let validFormat = false;
+ for (const canvasFormat of kCanvasTextureFormats) {
+ if (format === canvasFormat) {
+ validFormat = true;
+ break;
+ }
+ }
+
+ t.expectValidationError(() => {
+ ctx.configure({
+ device: t.device,
+ format,
+ });
+ }, !validFormat);
+
+ t.expectValidationError(() => {
+ // Should always return a texture, whether the configured format was valid or not.
+ const currentTexture = ctx.getCurrentTexture();
+ t.expect(currentTexture instanceof GPUTexture);
+ }, !validFormat);
+ });
+
+g.test('usage')
+ .desc(
+ `
+ Ensure that getCurrentTexture returns a texture with the configured usages.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .beginSubcases()
+ .expand('usage', p => {
+ const usageSet = new Set<number>();
+ for (const usage0 of kTextureUsages) {
+ for (const usage1 of kTextureUsages) {
+ usageSet.add(usage0 | usage1);
+ }
+ }
+ return usageSet;
+ })
+ )
+ .fn(async t => {
+ const { canvasType, usage } = t.params;
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ usage,
+ });
+
+ const currentTexture = ctx.getCurrentTexture();
+ t.expect(currentTexture instanceof GPUTexture);
+ t.expect(currentTexture.usage === usage);
+
+ // Try to use the texture with the given usage
+
+ if (usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) {
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: currentTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ if (usage & GPUConst.TextureUsage.TEXTURE_BINDING) {
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ texture: {},
+ },
+ ],
+ });
+
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: currentTexture.createView(),
+ },
+ ],
+ });
+ }
+
+ if (usage & GPUConst.TextureUsage.STORAGE_BINDING) {
+ const bgl = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ storageTexture: { access: 'write-only', format: currentTexture.format },
+ },
+ ],
+ });
+
+ t.device.createBindGroup({
+ layout: bgl,
+ entries: [
+ {
+ binding: 0,
+ resource: currentTexture.createView(),
+ },
+ ],
+ });
+ }
+
+ if (usage & GPUConst.TextureUsage.COPY_DST) {
+ const rgbaData = new Uint8Array([255, 0, 0, 255]);
+
+ t.device.queue.writeTexture({ texture: currentTexture }, rgbaData, {}, [1, 1, 1]);
+ }
+
+ if (usage & GPUConst.TextureUsage.COPY_SRC) {
+ const size = [currentTexture.width, currentTexture.height, 1];
+ const dstTexture = t.device.createTexture({
+ format: currentTexture.format,
+ usage: GPUTextureUsage.COPY_DST,
+ size,
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyTextureToTexture({ texture: currentTexture }, { texture: dstTexture }, size);
+ t.device.queue.submit([encoder.finish()]);
+ }
+ });
+
+g.test('alpha_mode')
+ .desc(
+ `
+ Ensure that all valid alphaMode values are allowed when calling configure.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .beginSubcases()
+ .combine('alphaMode', ['opaque', 'premultiplied'] as const)
+ )
+ .fn(async t => {
+ const { canvasType, alphaMode } = t.params;
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ alphaMode,
+ });
+
+ const currentTexture = ctx.getCurrentTexture();
+ t.expect(currentTexture instanceof GPUTexture);
+ });
+
+g.test('size_zero_before_configure')
+ .desc(`Ensure a validation error is raised in configure() if the size of the canvas is zero.`)
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('zeroDimension', ['width', 'height'] as const)
+ )
+ .fn(t => {
+ const { canvasType, zeroDimension } = t.params;
+ const canvas = createCanvas(t, canvasType, 1, 1);
+ canvas[zeroDimension] = 0;
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ // Validation error, the canvas size is 0 which doesn't make a valid GPUTextureDescriptor.
+ t.expectValidationError(() => {
+ ctx.configure({
+ device: t.device,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ });
+
+ canvas[zeroDimension] = 1;
+
+ // The size being incorrect doesn't make for an invalid configuration. Now that it is fixed
+ // getting textures from the canvas should work.
+ const currentTexture = ctx.getCurrentTexture();
+
+ // Try rendering to it even!
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: currentTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ });
+
+g.test('size_zero_after_configure')
+ .desc(
+ `Ensure a validation error is raised after configure() if the size of the canvas becomes zero.`
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('zeroDimension', ['width', 'height'] as const)
+ )
+ .fn(t => {
+ const { canvasType, zeroDimension } = t.params;
+ const canvas = createCanvas(t, canvasType, 1, 1);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: t.device,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ canvas[zeroDimension] = 0;
+
+ // The size is incorrect, we should be getting an error texture and a validation error.
+ let currentTexture: GPUTexture;
+ t.expectValidationError(() => {
+ currentTexture = ctx.getCurrentTexture();
+ });
+
+ t.expect(currentTexture![zeroDimension] === 0);
+
+ // Using the texture should produce a validation error.
+ t.expectValidationError(() => {
+ currentTexture.createView();
+ });
+ });
+
+g.test('viewFormats')
+ .desc(
+ `Test the validation that viewFormats are compatible with the format (for all canvas format / view formats)`
+ )
+ .params(u =>
+ u
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('format', kCanvasTextureFormats)
+ .combine('viewFormatFeature', kFeaturesForFormats)
+ .beginSubcases()
+ .expand('viewFormat', ({ viewFormatFeature }) =>
+ filterFormatsByFeature(viewFormatFeature, kTextureFormats)
+ )
+ )
+ .beforeAllSubcases(t => {
+ t.selectDeviceOrSkipTestCase([t.params.viewFormatFeature]);
+ })
+ .fn(t => {
+ const { canvasType, format, viewFormat } = t.params;
+ const canvas = createCanvas(t, canvasType, 1, 1);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ const compatible = viewCompatible(format, viewFormat);
+
+ // Test configure() produces an error if the formats aren't compatible.
+ t.expectValidationError(() => {
+ ctx.configure({
+ device: t.device,
+ format,
+ viewFormats: [viewFormat],
+ });
+ }, !compatible);
+
+ // Likewise for getCurrentTexture().
+ let currentTexture: GPUTexture;
+ t.expectValidationError(() => {
+ currentTexture = ctx.getCurrentTexture();
+ }, !compatible);
+
+ // The returned texture is an error texture.
+ t.expectValidationError(() => {
+ currentTexture.createView();
+ }, !compatible);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/context_creation.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/context_creation.spec.ts
new file mode 100644
index 0000000000..fc8a86c7a3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/context_creation.spec.ts
@@ -0,0 +1,47 @@
+export const description = `
+Tests for canvas context creation.
+
+Note there are no context creation attributes for WebGPU (as of this writing).
+Options are configured in configure() instead.
+`;
+
+import { Fixture } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+export const g = makeTestGroup(Fixture);
+
+g.test('return_type')
+ .desc(
+ `Test the return type of getContext for WebGPU.
+
+ TODO: Test OffscreenCanvas made from transferControlToOffscreen.`
+ )
+ .params(u =>
+ u //
+ .combine('offscreen', [false, true])
+ .beginSubcases()
+ .combine('attributes', [undefined, {}])
+ )
+ .fn(async t => {
+ let canvas: HTMLCanvasElement | OffscreenCanvas;
+ if (t.params.offscreen) {
+ if (typeof OffscreenCanvas === 'undefined') {
+ // Skip if the current context doesn't have OffscreenCanvas (e.g. Node).
+ t.skip('OffscreenCanvas is not available in this context');
+ }
+
+ canvas = new OffscreenCanvas(10, 10);
+ } else {
+ if (typeof document === 'undefined') {
+ // Skip if there is no document (Workers, Node)
+ t.skip('DOM is not available to create canvas element');
+ }
+
+ canvas = document.createElement('canvas', t.params.attributes);
+ canvas.width = 10;
+ canvas.height = 10;
+ }
+
+ const ctx = canvas.getContext('webgpu');
+ t.expect(ctx instanceof GPUCanvasContext);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts
new file mode 100644
index 0000000000..4a9507abe9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts
@@ -0,0 +1,262 @@
+export const description = `
+Tests for GPUCanvasContext.getCurrentTexture.
+`;
+
+import { SkipTestCase } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert, unreachable } from '../../../common/util/util.js';
+import { GPUTest } from '../../gpu_test.js';
+import { kAllCanvasTypes, createCanvas, CanvasType } from '../../util/create_elements.js';
+
+class GPUContextTest extends GPUTest {
+ initCanvasContext(canvasType: CanvasType = 'onscreen'): GPUCanvasContext {
+ const canvas = createCanvas(this, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: this.device,
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+
+ return ctx;
+ }
+}
+
+export const g = makeTestGroup(GPUContextTest);
+
+g.test('configured')
+ .desc(
+ `Checks that calling getCurrentTexture requires the context to be configured first, and
+ that each call to configure causes getCurrentTexture to return a new texture.`
+ )
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const canvas = createCanvas(t, t.params.canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ // Calling getCurrentTexture prior to configuration should throw an exception.
+ t.shouldThrow(true, () => {
+ ctx.getCurrentTexture();
+ });
+
+ // Once the context has been configured getCurrentTexture can be called.
+ ctx.configure({
+ device: t.device,
+ format: 'rgba8unorm',
+ });
+
+ let prevTexture = ctx.getCurrentTexture();
+
+ // Calling configure again with different values will change the texture returned.
+ ctx.configure({
+ device: t.device,
+ format: 'bgra8unorm',
+ });
+
+ let currentTexture = ctx.getCurrentTexture();
+ t.expect(prevTexture !== currentTexture);
+ prevTexture = currentTexture;
+
+ // Calling configure again with the same values will still change the texture returned.
+ ctx.configure({
+ device: t.device,
+ format: 'bgra8unorm',
+ });
+
+ currentTexture = ctx.getCurrentTexture();
+ t.expect(prevTexture !== currentTexture);
+ prevTexture = currentTexture;
+
+ // Calling getCurrentTexture after calling unconfigure should throw an exception.
+ ctx.unconfigure();
+
+ t.shouldThrow(true, () => {
+ ctx.getCurrentTexture();
+ });
+ });
+
+g.test('single_frames')
+ .desc(`Checks that the value of getCurrentTexture is consistent within a single frame.`)
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const ctx = t.initCanvasContext(t.params.canvasType);
+ const frameTexture = ctx.getCurrentTexture();
+
+ // Calling getCurrentTexture a second time returns the same texture.
+ t.expect(frameTexture === ctx.getCurrentTexture());
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: frameTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // Calling getCurrentTexture after performing some work on the texture returns the same texture.
+ t.expect(frameTexture === ctx.getCurrentTexture());
+
+ // Ensure that getCurrentTexture does not clear the texture.
+ t.expectSingleColor(frameTexture, frameTexture.format, {
+ size: [frameTexture.width, frameTexture.height, 1],
+ exp: { R: 1, G: 0, B: 0, A: 1 },
+ });
+
+ frameTexture.destroy();
+
+ // Calling getCurrentTexture after destroying the texture still returns the same texture.
+ t.expect(frameTexture === ctx.getCurrentTexture());
+ });
+
+g.test('multiple_frames')
+ .desc(`Checks that the value of getCurrentTexture differs across multiple frames.`)
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ .beginSubcases()
+ .combine('clearTexture', [true, false])
+ )
+ .beforeAllSubcases(t => {
+ const { canvasType } = t.params;
+ if (canvasType === 'offscreen' && !('transferToImageBitmap' in OffscreenCanvas.prototype)) {
+ throw new SkipTestCase('transferToImageBitmap not supported');
+ }
+ })
+ .fn(async t => {
+ const { canvasType, clearTexture } = t.params;
+
+ return new Promise(resolve => {
+ const ctx = t.initCanvasContext(canvasType);
+ let prevTexture: GPUTexture | undefined;
+ let frameCount = 0;
+
+ async function frameCheck() {
+ const currentTexture = ctx.getCurrentTexture();
+
+ if (prevTexture) {
+ // Ensure that each frame a new texture object is returned.
+ t.expect(currentTexture !== prevTexture);
+
+ // Ensure that texture contents are transparent black.
+ t.expectSingleColor(currentTexture, currentTexture.format, {
+ size: [currentTexture.width, currentTexture.height, 1],
+ exp: { R: 0, G: 0, B: 0, A: 0 },
+ });
+ }
+
+ if (clearTexture) {
+ // Clear the texture to test that texture contents don't carry over from frame to frame.
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: currentTexture.createView(),
+ clearValue: [1.0, 0.0, 0.0, 1.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ prevTexture = currentTexture;
+
+ if (frameCount++ < 5) {
+ // Which method will be used to begin a new "frame"?
+ switch (canvasType) {
+ case 'onscreen':
+ requestAnimationFrame(frameCheck);
+ break;
+ case 'offscreen': {
+ (ctx.canvas as OffscreenCanvas).transferToImageBitmap();
+ // The beginning of frameCheck runs immediately (in the same task), so this
+ // verifies the state has changed synchronously.
+ void frameCheck();
+ break;
+ }
+ default:
+ unreachable();
+ }
+ } else {
+ resolve();
+ }
+ }
+
+ void frameCheck();
+ });
+ });
+
+g.test('resize')
+ .desc(`Checks the value of getCurrentTexture differs when the canvas is resized.`)
+ .params(u =>
+ u //
+ .combine('canvasType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const ctx = t.initCanvasContext(t.params.canvasType);
+ let prevTexture = ctx.getCurrentTexture();
+
+ // Trigger a resize by changing the width.
+ ctx.canvas.width = 4;
+
+ // When the canvas resizes the texture returned by getCurrentTexture should immediately begin
+ // returning a new texture matching the update dimensions.
+ let currentTexture = ctx.getCurrentTexture();
+ t.expect(prevTexture !== currentTexture);
+ t.expect(currentTexture.width === ctx.canvas.width);
+ t.expect(currentTexture.height === ctx.canvas.height);
+
+ // The width and height of the previous texture should remain unchanged.
+ t.expect(prevTexture.width === 2);
+ t.expect(prevTexture.height === 2);
+ prevTexture = currentTexture;
+
+ // Ensure that texture contents are transparent black.
+ t.expectSingleColor(currentTexture, currentTexture.format, {
+ size: [currentTexture.width, currentTexture.height, 1],
+ exp: { R: 0, G: 0, B: 0, A: 0 },
+ });
+
+ // Trigger a resize by changing the height.
+ ctx.canvas.height = 4;
+
+ // Check to ensure the texture is resized again.
+ currentTexture = ctx.getCurrentTexture();
+ t.expect(prevTexture !== currentTexture);
+ t.expect(currentTexture.width === ctx.canvas.width);
+ t.expect(currentTexture.height === ctx.canvas.height);
+ t.expect(prevTexture.width === 4);
+ t.expect(prevTexture.height === 2);
+ prevTexture = currentTexture;
+
+ // Ensure that texture contents are transparent black.
+ t.expectSingleColor(currentTexture, currentTexture.format, {
+ size: [currentTexture.width, currentTexture.height, 1],
+ exp: { R: 0, G: 0, B: 0, A: 0 },
+ });
+
+ // Simply setting the canvas width and height values to their current values should not trigger
+ // a change in the texture.
+ ctx.canvas.width = 4;
+ ctx.canvas.height = 4;
+
+ currentTexture = ctx.getCurrentTexture();
+ t.expect(prevTexture === currentTexture);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getPreferredCanvasFormat.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getPreferredCanvasFormat.spec.ts
new file mode 100644
index 0000000000..0d966bc812
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getPreferredCanvasFormat.spec.ts
@@ -0,0 +1,19 @@
+export const description = `
+Tests for navigator.gpu.getPreferredCanvasFormat.
+`;
+
+import { Fixture } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+
+export const g = makeTestGroup(Fixture);
+
+g.test('value')
+ .desc(
+ `
+ Ensure getPreferredCanvasFormat returns one of the valid values.
+ `
+ )
+ .fn(async t => {
+ const preferredFormat = navigator.gpu.getPreferredCanvasFormat();
+ t.expect(preferredFormat === 'bgra8unorm' || preferredFormat === 'rgba8unorm');
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts
new file mode 100644
index 0000000000..61ce6ff55b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/readbackFromWebGPUCanvas.spec.ts
@@ -0,0 +1,473 @@
+export const description = `
+Tests for readback from WebGPU Canvas.
+
+This includes testing that colorSpace makes it through from the WebGPU canvas
+to the form of copy (toDataURL, toBlob, ImageBitmap, drawImage)
+
+The color space support is tested by drawing the readback form of the WebGPU
+canvas into a 2D canvas of a different color space via drawImage (A). Another
+2D canvas is created with the same source data and color space as the WebGPU
+canvas and also drawn into another 2D canvas of a different color space (B).
+The contents of A and B should match.
+
+TODO: implement all canvas types, see TODO on kCanvasTypes.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert, raceWithRejectOnTimeout, unreachable } from '../../../common/util/util.js';
+import {
+ kCanvasAlphaModes,
+ kCanvasColorSpaces,
+ kCanvasTextureFormats,
+} from '../../capability_info.js';
+import { GPUTest } from '../../gpu_test.js';
+import { checkElementsEqual } from '../../util/check_contents.js';
+import {
+ kAllCanvasTypes,
+ CanvasType,
+ createCanvas,
+ createOnscreenCanvas,
+} from '../../util/create_elements.js';
+
+export const g = makeTestGroup(GPUTest);
+
+// We choose 0x66 as the value for each color and alpha channel
+// 0x66 / 0xff = 0.4
+// Given a pixel value of RGBA = (0x66, 0, 0, 0x66) in the source WebGPU canvas,
+// For alphaMode = opaque, the copy output should be RGBA = (0x66, 0, 0, 0xff)
+// For alphaMode = premultiplied, the copy output should be RGBA = (0xff, 0, 0, 0x66)
+const kPixelValue = 0x66;
+const kPixelValueFloat = 0x66 / 0xff; // 0.4
+
+// Use four pixels rectangle for the test:
+// blue: top-left;
+// green: top-right;
+// red: bottom-left;
+// yellow: bottom-right;
+const expect = {
+ /* prettier-ignore */
+ 'opaque': new Uint8ClampedArray([
+ 0, 0, kPixelValue, 0xff, // blue
+ 0, kPixelValue, 0, 0xff, // green
+ kPixelValue, 0, 0, 0xff, // red
+ kPixelValue, kPixelValue, 0, 0xff, // yellow
+ ]),
+ /* prettier-ignore */
+ 'premultiplied': new Uint8ClampedArray([
+ 0, 0, 0xff, kPixelValue, // blue
+ 0, 0xff, 0, kPixelValue, // green
+ 0xff, 0, 0, kPixelValue, // red
+ 0xff, 0xff, 0, kPixelValue, // yellow
+ ]),
+};
+
+async function initWebGPUCanvasContent<T extends CanvasType>(
+ t: GPUTest,
+ format: GPUTextureFormat,
+ alphaMode: GPUCanvasAlphaMode,
+ colorSpace: PredefinedColorSpace,
+ canvasType: T
+) {
+ const canvas = createCanvas(t, canvasType, 2, 2);
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ ctx.configure({
+ device: t.device,
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
+ alphaMode,
+ colorSpace,
+ });
+
+ const canvasTexture = ctx.getCurrentTexture();
+ const tempTexture = t.device.createTexture({
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
+ format,
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const tempTextureView = tempTexture.createView();
+ const encoder = t.device.createCommandEncoder();
+
+ const clearOnePixel = (origin: GPUOrigin3D, color: GPUColor) => {
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ { view: tempTextureView, clearValue: color, loadOp: 'clear', storeOp: 'store' },
+ ],
+ });
+ pass.end();
+ encoder.copyTextureToTexture(
+ { texture: tempTexture },
+ { texture: canvasTexture, origin },
+ { width: 1, height: 1 }
+ );
+ };
+
+ clearOnePixel([0, 0], [0, 0, kPixelValueFloat, kPixelValueFloat]);
+ clearOnePixel([1, 0], [0, kPixelValueFloat, 0, kPixelValueFloat]);
+ clearOnePixel([0, 1], [kPixelValueFloat, 0, 0, kPixelValueFloat]);
+ clearOnePixel([1, 1], [kPixelValueFloat, kPixelValueFloat, 0, kPixelValueFloat]);
+
+ t.device.queue.submit([encoder.finish()]);
+ tempTexture.destroy();
+
+ return canvas;
+}
+
+function drawImageSourceIntoCanvas(
+ t: GPUTest,
+ image: CanvasImageSource,
+ colorSpace: PredefinedColorSpace
+) {
+ const canvas: HTMLCanvasElement = createOnscreenCanvas(t, 2, 2);
+ const ctx = canvas.getContext('2d', { colorSpace });
+ assert(ctx !== null);
+ ctx.drawImage(image, 0, 0);
+ return ctx;
+}
+
+function checkImageResultWithSameColorSpaceCanvas(
+ t: GPUTest,
+ image: CanvasImageSource,
+ sourceColorSpace: PredefinedColorSpace,
+ expect: Uint8ClampedArray
+) {
+ const ctx = drawImageSourceIntoCanvas(t, image, sourceColorSpace);
+ readPixelsFrom2DCanvasAndCompare(t, ctx, expect);
+}
+
+function checkImageResultWithDifferentColorSpaceCanvas(
+ t: GPUTest,
+ image: CanvasImageSource,
+ sourceColorSpace: PredefinedColorSpace,
+ sourceData: Uint8ClampedArray
+) {
+ const destinationColorSpace = sourceColorSpace === 'srgb' ? 'display-p3' : 'srgb';
+
+ // draw the WebGPU derived data into a canvas
+ const fromWebGPUCtx = drawImageSourceIntoCanvas(t, image, destinationColorSpace);
+
+ // create a 2D canvas with the same source data in the same color space as the WebGPU
+ // canvas
+ const source2DCanvas: HTMLCanvasElement = createOnscreenCanvas(t, 2, 2);
+ const source2DCtx = source2DCanvas.getContext('2d', { colorSpace: sourceColorSpace });
+ assert(source2DCtx !== null);
+ const imgData = source2DCtx.getImageData(0, 0, 2, 2);
+ imgData.data.set(sourceData);
+ source2DCtx.putImageData(imgData, 0, 0);
+
+ // draw the source 2D canvas into another 2D canvas with the destination color space and
+ // then pull out the data. This result should be the same as the WebGPU derived data
+ // written to a 2D canvas of the same destination color space.
+ const from2DCtx = drawImageSourceIntoCanvas(t, source2DCanvas, destinationColorSpace);
+ const expect = from2DCtx.getImageData(0, 0, 2, 2).data;
+
+ readPixelsFrom2DCanvasAndCompare(t, fromWebGPUCtx, expect);
+}
+
+function checkImageResult(
+ t: GPUTest,
+ image: CanvasImageSource,
+ sourceColorSpace: PredefinedColorSpace,
+ expect: Uint8ClampedArray
+) {
+ checkImageResultWithSameColorSpaceCanvas(t, image, sourceColorSpace, expect);
+ checkImageResultWithDifferentColorSpaceCanvas(t, image, sourceColorSpace, expect);
+}
+
+function readPixelsFrom2DCanvasAndCompare(
+ t: GPUTest,
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ expect: Uint8ClampedArray
+) {
+ const actual = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
+
+ t.expectOK(checkElementsEqual(actual, expect));
+}
+
+g.test('onscreenCanvas,snapshot')
+ .desc(
+ `
+ Ensure snapshot of canvas with WebGPU context is correct with
+ - various WebGPU canvas texture formats
+ - WebGPU canvas alpha mode = {"opaque", "premultiplied"}
+ - colorSpace = {"srgb", "display-p3"}
+ - snapshot methods = {convertToBlob, transferToImageBitmap, createImageBitmap}
+
+ TODO: Snapshot canvas to jpeg, webp and other mime type and
+ different quality. Maybe we should test them in reftest.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kCanvasTextureFormats)
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('colorSpace', kCanvasColorSpaces)
+ .combine('snapshotType', ['toDataURL', 'toBlob', 'imageBitmap'])
+ )
+ .fn(async t => {
+ const canvas = await initWebGPUCanvasContent(
+ t,
+ t.params.format,
+ t.params.alphaMode,
+ t.params.colorSpace,
+ 'onscreen'
+ );
+
+ let snapshot: HTMLImageElement | ImageBitmap;
+ switch (t.params.snapshotType) {
+ case 'toDataURL': {
+ const url = canvas.toDataURL();
+ const img = new Image(canvas.width, canvas.height);
+ img.src = url;
+ await raceWithRejectOnTimeout(img.decode(), 5000, 'load image timeout');
+ snapshot = img;
+ break;
+ }
+ case 'toBlob': {
+ const blobFromCanvas = new Promise(resolve => {
+ canvas.toBlob(blob => resolve(blob));
+ });
+ const blob = (await blobFromCanvas) as Blob;
+ const url = URL.createObjectURL(blob);
+ const img = new Image(canvas.width, canvas.height);
+ img.src = url;
+ await raceWithRejectOnTimeout(img.decode(), 5000, 'load image timeout');
+ snapshot = img;
+ break;
+ }
+ case 'imageBitmap': {
+ snapshot = await createImageBitmap(canvas);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ checkImageResult(t, snapshot, t.params.colorSpace, expect[t.params.alphaMode]);
+ });
+
+g.test('offscreenCanvas,snapshot')
+ .desc(
+ `
+ Ensure snapshot of offscreenCanvas with WebGPU context is correct with
+ - various WebGPU canvas texture formats
+ - WebGPU canvas alpha mode = {"opaque", "premultiplied"}
+ - colorSpace = {"srgb", "display-p3"}
+ - snapshot methods = {convertToBlob, transferToImageBitmap, createImageBitmap}
+
+ TODO: Snapshot offscreenCanvas to jpeg, webp and other mime type and
+ different quality. Maybe we should test them in reftest.
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kCanvasTextureFormats)
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('colorSpace', kCanvasColorSpaces)
+ .combine('snapshotType', ['convertToBlob', 'transferToImageBitmap', 'imageBitmap'])
+ )
+ .fn(async t => {
+ const offscreenCanvas = await initWebGPUCanvasContent(
+ t,
+ t.params.format,
+ t.params.alphaMode,
+ t.params.colorSpace,
+ 'offscreen'
+ );
+
+ let snapshot: HTMLImageElement | ImageBitmap;
+ switch (t.params.snapshotType) {
+ case 'convertToBlob': {
+ if (typeof offscreenCanvas.convertToBlob === undefined) {
+ t.skip("Browser doesn't support OffscreenCanvas.convertToBlob");
+ return;
+ }
+ const blob = await offscreenCanvas.convertToBlob();
+ const url = URL.createObjectURL(blob);
+ const img = new Image(offscreenCanvas.width, offscreenCanvas.height);
+ img.src = url;
+ await raceWithRejectOnTimeout(img.decode(), 5000, 'load image timeout');
+ snapshot = img;
+ break;
+ }
+ case 'transferToImageBitmap': {
+ if (typeof offscreenCanvas.transferToImageBitmap === undefined) {
+ t.skip("Browser doesn't support OffscreenCanvas.transferToImageBitmap");
+ return;
+ }
+ snapshot = offscreenCanvas.transferToImageBitmap();
+ break;
+ }
+ case 'imageBitmap': {
+ snapshot = await createImageBitmap(offscreenCanvas);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ checkImageResult(t, snapshot, t.params.colorSpace, expect[t.params.alphaMode]);
+ });
+
+g.test('onscreenCanvas,uploadToWebGL')
+ .desc(
+ `
+ Ensure upload WebGPU context canvas to webgl texture is correct with
+ - various WebGPU canvas texture formats
+ - WebGPU canvas alpha mode = {"opaque", "premultiplied"}
+ - upload methods = {texImage2D, texSubImage2D}
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kCanvasTextureFormats)
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('webgl', ['webgl', 'webgl2'])
+ .combine('upload', ['texImage2D', 'texSubImage2D'])
+ )
+ .fn(async t => {
+ const { format, webgl, upload } = t.params;
+ const canvas = await initWebGPUCanvasContent(t, format, t.params.alphaMode, 'srgb', 'onscreen');
+
+ const expectCanvas: HTMLCanvasElement = createOnscreenCanvas(t, canvas.width, canvas.height);
+ const gl = expectCanvas.getContext(webgl) as WebGLRenderingContext | WebGL2RenderingContext;
+ if (gl === null) {
+ return;
+ }
+
+ const texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ switch (upload) {
+ case 'texImage2D': {
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
+ break;
+ }
+ case 'texSubImage2D': {
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ canvas.width,
+ canvas.height,
+ 0,
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ null
+ );
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
+ break;
+ }
+ default:
+ unreachable();
+ }
+
+ const fb = gl.createFramebuffer();
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
+
+ const pixels = new Uint8Array(canvas.width * canvas.height * 4);
+ gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+ const actual = new Uint8ClampedArray(pixels);
+
+ t.expectOK(checkElementsEqual(actual, expect[t.params.alphaMode]));
+ });
+
+g.test('drawTo2DCanvas')
+ .desc(
+ `
+ Ensure draw WebGPU context canvas to 2d context canvas/offscreenCanvas is correct with
+ - various WebGPU canvas texture formats
+ - WebGPU canvas alpha mode = {"opaque", "premultiplied"}
+ - colorSpace = {"srgb", "display-p3"}
+ - WebGPU canvas type = {"onscreen", "offscreen"}
+ - 2d canvas type = {"onscreen", "offscreen"}
+ `
+ )
+ .params(u =>
+ u //
+ .combine('format', kCanvasTextureFormats)
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('colorSpace', kCanvasColorSpaces)
+ .combine('webgpuCanvasType', kAllCanvasTypes)
+ .combine('canvas2DType', kAllCanvasTypes)
+ )
+ .fn(async t => {
+ const { format, webgpuCanvasType, alphaMode, colorSpace, canvas2DType } = t.params;
+
+ const canvas = await initWebGPUCanvasContent(
+ t,
+ format,
+ alphaMode,
+ colorSpace,
+ webgpuCanvasType
+ );
+
+ const expectCanvas = createCanvas(t, canvas2DType, canvas.width, canvas.height);
+ const ctx = expectCanvas.getContext('2d') as CanvasRenderingContext2D;
+ if (ctx === null) {
+ t.skip(canvas2DType + ' canvas cannot get 2d context');
+ return;
+ }
+
+ ctx.drawImage(canvas, 0, 0);
+ readPixelsFrom2DCanvasAndCompare(t, ctx, expect[t.params.alphaMode]);
+ });
+
+g.test('transferToImageBitmap_unconfigured_nonzero_size')
+ .desc(
+ `Regression test for a crash when calling transferImageBitmap on an unconfigured. Case where the canvas is not empty`
+ )
+ .fn(t => {
+ const canvas = createCanvas(t, 'offscreen', 2, 3);
+ canvas.getContext('webgpu');
+
+ // Transferring gives an ImageBitmap of the correct size filled with transparent black.
+ const ib = canvas.transferToImageBitmap();
+ t.expect(ib.width === canvas.width);
+ t.expect(ib.height === canvas.height);
+
+ const readbackCanvas = document.createElement('canvas');
+ readbackCanvas.width = canvas.width;
+ readbackCanvas.height = canvas.height;
+ const readbackContext = readbackCanvas.getContext('2d', {
+ alpha: true,
+ });
+ if (readbackContext === null) {
+ t.skip('Cannot get a 2D canvas context');
+ return;
+ }
+
+ // Since there isn't a configuration we expect the ImageBitmap to have the default alphaMode of "opaque".
+ const expected = new Uint8ClampedArray(canvas.width * canvas.height * 4);
+ for (let i = 0; i < expected.byteLength; i += 4) {
+ expected[i + 0] = 0;
+ expected[i + 1] = 0;
+ expected[i + 2] = 0;
+ expected[i + 3] = 255;
+ }
+
+ readbackContext.drawImage(ib, 0, 0);
+ readPixelsFrom2DCanvasAndCompare(t, readbackContext, expected);
+ });
+
+g.test('transferToImageBitmap_zero_size')
+ .desc(
+ `Regression test for a crash when calling transferImageBitmap on an unconfigured. Case where the canvas is empty.`
+ )
+ .params(u => u.combine('configure', [true, false]))
+ .fn(t => {
+ const { configure } = t.params;
+ const canvas = createCanvas(t, 'offscreen', 0, 1);
+ const ctx = canvas.getContext('webgpu')!;
+
+ if (configure) {
+ t.expectValidationError(() => ctx.configure({ device: t.device, format: 'bgra8unorm' }));
+ }
+
+ // Transferring would give an empty ImageBitmap which is not possible, so an Exception is thrown.
+ t.shouldThrow(true, () => {
+ canvas.transferToImageBitmap();
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/ImageBitmap.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/ImageBitmap.spec.ts
new file mode 100644
index 0000000000..825a3c706b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/ImageBitmap.spec.ts
@@ -0,0 +1,581 @@
+export const description = `
+copyExternalImageToTexture from ImageBitmaps created from various sources.
+
+TODO: Test ImageBitmap generated from all possible ImageBitmapSource, relevant ImageBitmapOptions
+ (https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#images-2)
+ and various source filetypes and metadata (weird dimensions, EXIF orientations, video rotations
+ and visible/crop rectangles, etc. (In theory these things are handled inside createImageBitmap,
+ but in theory could affect the internal representation of the ImageBitmap.)
+
+TODO: Test zero-sized copies from all sources (just make sure params cover it) (e.g. 0x0, 0x4, 4x0).
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import {
+ kTextureFormatInfo,
+ kValidTextureFormatsForCopyE2T,
+ EncodableTextureFormat,
+} from '../../capability_info.js';
+import { CopyToTextureUtils, kCopySubrectInfo } from '../../util/copy_to_texture.js';
+import { PerTexelComponent } from '../../util/texture/texel_data.js';
+import { TexelView } from '../../util/texture/texel_view.js';
+
+type TestColor = PerTexelComponent<number>;
+// None of the dst texture format is 'uint' or 'sint', so we can always use float value.
+const kColors = {
+ Red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 },
+ Green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
+ Blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 },
+ Black: { R: 0.0, G: 0.0, B: 0.0, A: 1.0 },
+ White: { R: 1.0, G: 1.0, B: 1.0, A: 1.0 },
+ SemitransparentWhite: { R: 1.0, G: 1.0, B: 1.0, A: 0.6 },
+} as const;
+const kTestColorsOpaque = [
+ kColors.Red,
+ kColors.Green,
+ kColors.Blue,
+ kColors.Black,
+ kColors.White,
+] as const;
+const kTestColorsAll = [...kTestColorsOpaque, kColors.SemitransparentWhite] as const;
+
+function makeTestColorsTexelView({
+ testColors,
+ format,
+ width,
+ height,
+ premultiplied,
+ flipY,
+}: {
+ testColors: readonly TestColor[];
+ format: EncodableTextureFormat;
+ width: number;
+ height: number;
+ premultiplied: boolean;
+ flipY: boolean;
+}) {
+ return TexelView.fromTexelsAsColors(format, coords => {
+ const y = flipY ? height - coords.y - 1 : coords.y;
+ const pixelPos = y * width + coords.x;
+ const currentPixel = testColors[pixelPos % testColors.length];
+
+ if (premultiplied && currentPixel.A !== 1.0) {
+ return {
+ R: currentPixel.R! * currentPixel.A!,
+ G: currentPixel.G! * currentPixel.A!,
+ B: currentPixel.B! * currentPixel.A!,
+ A: currentPixel.A,
+ };
+ } else {
+ return currentPixel;
+ }
+ });
+}
+
+export const g = makeTestGroup(CopyToTextureUtils);
+
+g.test('from_ImageData')
+ .desc(
+ `
+ Test ImageBitmap generated from ImageData can be copied to WebGPU
+ texture correctly. These imageBitmaps are highly possible living
+ in CPU back resource.
+
+ It generates pixels in ImageData one by one based on a color list:
+ [Red, Green, Blue, Black, White, SemitransparentWhite].
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the ImageBitmap contents.
+
+ Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and do unpremultiply alpha if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ The tests covers:
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('alpha', ['none', 'premultiply'] as const)
+ .combine('orientation', ['none', 'flipY'] as const)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ .combine('dstPremultiplied', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15, 255, 256])
+ .combine('height', [1, 2, 4, 15, 255, 256])
+ )
+ .fn(async t => {
+ const {
+ width,
+ height,
+ alpha,
+ orientation,
+ dstColorFormat,
+ dstPremultiplied,
+ srcDoFlipYDuringCopy,
+ } = t.params;
+
+ const testColors = kTestColorsAll;
+
+ // Generate correct expected values
+ const texelViewSource = makeTestColorsTexelView({
+ testColors,
+ format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
+ width,
+ height,
+ flipY: false,
+ premultiplied: false,
+ });
+ const imageData = new ImageData(width, height);
+ texelViewSource.writeTextureData(imageData.data, {
+ bytesPerRow: width * 4,
+ rowsPerImage: height,
+ subrectOrigin: [0, 0],
+ subrectSize: { width, height },
+ });
+
+ const imageBitmap = await createImageBitmap(imageData, {
+ premultiplyAlpha: alpha,
+ imageOrientation: orientation,
+ });
+
+ const dst = t.device.createTexture({
+ size: { width, height },
+ format: dstColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const expFormat = kTextureFormatInfo[dstColorFormat].baseFormat ?? dstColorFormat;
+ const flipSrcBeforeCopy = orientation === 'flipY';
+ const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: imageData.data,
+ srcOrigin: [0, 0],
+ srcSize: [width, height],
+ dstOrigin: [0, 0],
+ dstSize: [width, height],
+ subRectSize: [width, height],
+ format: expFormat,
+ flipSrcBeforeCopy,
+ srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: false,
+ dstPremultiplied,
+ },
+ });
+
+ t.doTestAndCheckResult(
+ { source: imageBitmap, origin: { x: 0, y: 0 }, flipY: srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: { x: 0, y: 0 },
+ colorSpace: 'srgb',
+ premultipliedAlpha: dstPremultiplied,
+ },
+ texelViewExpected,
+ { width, height, depthOrArrayLayers: 1 },
+ // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
+ // allow diffs of 1ULP since that's the generally-appropriate threshold.
+ { maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
+ );
+ });
+
+g.test('from_canvas')
+ .desc(
+ `
+ Test ImageBitmap generated from canvas/offscreenCanvas can be copied to WebGPU
+ texture correctly. These imageBitmaps are highly possible living in GPU back resource.
+
+ It generates pixels in ImageData one by one based on a color list:
+ [Red, Green, Blue, Black, White].
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the ImageBitmap contents.
+
+ Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and do unpremultiply alpha if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ The tests covers:
+ - Valid 2D canvas
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('orientation', ['none', 'flipY'] as const)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ .combine('dstPremultiplied', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15, 255, 256])
+ .combine('height', [1, 2, 4, 15, 255, 256])
+ )
+ .fn(async t => {
+ const {
+ width,
+ height,
+ orientation,
+ dstColorFormat,
+ dstPremultiplied,
+ srcDoFlipYDuringCopy,
+ } = t.params;
+
+ // CTS sometimes runs on worker threads, where document is not available.
+ // In this case, OffscreenCanvas can be used instead of <canvas>.
+ // But some browsers don't support OffscreenCanvas, and some don't
+ // support '2d' contexts on OffscreenCanvas.
+ // In this situation, the case will be skipped.
+ let imageCanvas;
+ if (typeof document !== 'undefined') {
+ imageCanvas = document.createElement('canvas');
+ imageCanvas.width = width;
+ imageCanvas.height = height;
+ } else if (typeof OffscreenCanvas === 'undefined') {
+ t.skip('OffscreenCanvas is not supported');
+ return;
+ } else {
+ imageCanvas = new OffscreenCanvas(width, height);
+ }
+ const imageCanvasContext = imageCanvas.getContext('2d');
+ if (imageCanvasContext === null) {
+ t.skip('OffscreenCanvas "2d" context not available');
+ return;
+ }
+
+ // Generate non-transparent pixel data to avoid canvas
+ // different opt behaviour on putImageData()
+ // from browsers.
+ const texelViewSource = makeTestColorsTexelView({
+ testColors: kTestColorsOpaque,
+ format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
+ width,
+ height,
+ flipY: false,
+ premultiplied: false,
+ });
+ // Generate correct expected values
+ const imageData = new ImageData(width, height);
+ texelViewSource.writeTextureData(imageData.data, {
+ bytesPerRow: width * 4,
+ rowsPerImage: height,
+ subrectOrigin: [0, 0],
+ subrectSize: { width, height },
+ });
+
+ // Use putImageData to prevent color space conversion.
+ imageCanvasContext.putImageData(imageData, 0, 0);
+
+ // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
+ // `createImageBitmap` that takes `ImageBitmapOptions`.
+ const imageBitmap = await createImageBitmap(imageCanvas as HTMLCanvasElement, {
+ premultiplyAlpha: 'premultiply',
+ imageOrientation: orientation,
+ });
+
+ const dst = t.device.createTexture({
+ size: { width, height },
+ format: dstColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const expFormat = kTextureFormatInfo[dstColorFormat].baseFormat ?? dstColorFormat;
+ const flipSrcBeforeCopy = orientation === 'flipY';
+ const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: imageData.data,
+ srcOrigin: [0, 0],
+ srcSize: [width, height],
+ dstOrigin: [0, 0],
+ dstSize: [width, height],
+ subRectSize: [width, height],
+ format: expFormat,
+ flipSrcBeforeCopy,
+ srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: false,
+ dstPremultiplied,
+ },
+ });
+
+ t.doTestAndCheckResult(
+ { source: imageBitmap, origin: { x: 0, y: 0 }, flipY: srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: { x: 0, y: 0 },
+ colorSpace: 'srgb',
+ premultipliedAlpha: dstPremultiplied,
+ },
+ texelViewExpected,
+ { width, height, depthOrArrayLayers: 1 },
+ // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
+ // allow diffs of 1ULP since that's the generally-appropriate threshold.
+ { maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
+ );
+ });
+
+g.test('copy_subrect_from_ImageData')
+ .desc(
+ `
+ Test ImageBitmap generated from ImageData can be copied to WebGPU
+ texture correctly. These imageBitmaps are highly possible living in CPU back resource.
+
+ It generates pixels in ImageData one by one based on a color list:
+ [Red, Green, Blue, Black, White].
+
+ Then call copyExternalImageToTexture() to do a subrect copy, based on a predefined copy
+ rect info list, to the 0 mipLevel of dst texture, and read the contents out to compare
+ with the ImageBitmap contents.
+
+ Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and do unpremultiply alpha if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped, and origin is top-left consistantly.
+
+ The tests covers:
+ - Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+ - Valid subrect copies.
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('alpha', ['none', 'premultiply'] as const)
+ .combine('orientation', ['none', 'flipY'] as const)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .combine('dstPremultiplied', [true, false])
+ .beginSubcases()
+ .combine('copySubRectInfo', kCopySubrectInfo)
+ )
+ .fn(async t => {
+ const {
+ copySubRectInfo,
+ alpha,
+ orientation,
+ dstPremultiplied,
+ srcDoFlipYDuringCopy,
+ } = t.params;
+
+ const testColors = kTestColorsAll;
+ const { srcOrigin, dstOrigin, srcSize, dstSize, copyExtent } = copySubRectInfo;
+ const kColorFormat = 'rgba8unorm';
+
+ // Generate correct expected values
+ const texelViewSource = makeTestColorsTexelView({
+ testColors,
+ format: kColorFormat, // ImageData is always in rgba8unorm format.
+ width: srcSize.width,
+ height: srcSize.height,
+ flipY: false,
+ premultiplied: false,
+ });
+ const imageData = new ImageData(srcSize.width, srcSize.height);
+ texelViewSource.writeTextureData(imageData.data, {
+ bytesPerRow: srcSize.width * 4,
+ rowsPerImage: srcSize.height,
+ subrectOrigin: [0, 0],
+ subrectSize: srcSize,
+ });
+
+ const imageBitmap = await createImageBitmap(imageData, {
+ premultiplyAlpha: alpha,
+ imageOrientation: orientation,
+ });
+
+ const dst = t.device.createTexture({
+ size: dstSize,
+ format: kColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const flipSrcBeforeCopy = orientation === 'flipY';
+ const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: imageData.data,
+ srcOrigin,
+ srcSize,
+ dstOrigin,
+ dstSize,
+ subRectSize: copyExtent,
+ format: kColorFormat,
+ flipSrcBeforeCopy,
+ srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: false,
+ dstPremultiplied,
+ },
+ });
+
+ t.doTestAndCheckResult(
+ { source: imageBitmap, origin: srcOrigin, flipY: srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: dstOrigin,
+ colorSpace: 'srgb',
+ premultipliedAlpha: dstPremultiplied,
+ },
+ texelViewExpected,
+ copyExtent,
+ // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
+ // allow diffs of 1ULP since that's the generally-appropriate threshold.
+ { maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
+ );
+ });
+
+g.test('copy_subrect_from_2D_Canvas')
+ .desc(
+ `
+ Test ImageBitmap generated from canvas/offscreenCanvas can be copied to WebGPU
+ texture correctly. These imageBitmaps are highly possible living in GPU back resource.
+
+ It generates pixels in ImageData one by one based on a color list:
+ [Red, Green, Blue, Black, White].
+
+ Then call copyExternalImageToTexture() to do a subrect copy, based on a predefined copy
+ rect info list, to the 0 mipLevel of dst texture, and read the contents out to compare
+ with the ImageBitmap contents.
+
+ Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and do unpremultiply alpha if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped, and origin is top-left consistantly.
+
+ The tests covers:
+ - Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+ - Valid subrect copies.
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('orientation', ['none', 'flipY'] as const)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .combine('dstPremultiplied', [true, false])
+ .beginSubcases()
+ .combine('copySubRectInfo', kCopySubrectInfo)
+ )
+ .fn(async t => {
+ const { copySubRectInfo, orientation, dstPremultiplied, srcDoFlipYDuringCopy } = t.params;
+
+ const { srcOrigin, dstOrigin, srcSize, dstSize, copyExtent } = copySubRectInfo;
+ const kColorFormat = 'rgba8unorm';
+
+ // CTS sometimes runs on worker threads, where document is not available.
+ // In this case, OffscreenCanvas can be used instead of <canvas>.
+ // But some browsers don't support OffscreenCanvas, and some don't
+ // support '2d' contexts on OffscreenCanvas.
+ // In this situation, the case will be skipped.
+ let imageCanvas;
+ if (typeof document !== 'undefined') {
+ imageCanvas = document.createElement('canvas');
+ imageCanvas.width = srcSize.width;
+ imageCanvas.height = srcSize.height;
+ } else if (typeof OffscreenCanvas === 'undefined') {
+ t.skip('OffscreenCanvas is not supported');
+ return;
+ } else {
+ imageCanvas = new OffscreenCanvas(srcSize.width, srcSize.height);
+ }
+ const imageCanvasContext = imageCanvas.getContext('2d');
+ if (imageCanvasContext === null) {
+ t.skip('OffscreenCanvas "2d" context not available');
+ return;
+ }
+
+ // Generate non-transparent pixel data to avoid canvas
+ // different opt behaviour on putImageData()
+ // from browsers.
+ const texelViewSource = makeTestColorsTexelView({
+ testColors: kTestColorsOpaque,
+ format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
+ width: srcSize.width,
+ height: srcSize.height,
+ flipY: false,
+ premultiplied: false,
+ });
+ // Generate correct expected values
+ const imageData = new ImageData(srcSize.width, srcSize.height);
+ texelViewSource.writeTextureData(imageData.data, {
+ bytesPerRow: srcSize.width * 4,
+ rowsPerImage: srcSize.height,
+ subrectOrigin: [0, 0],
+ subrectSize: srcSize,
+ });
+
+ // Use putImageData to prevent color space conversion.
+ imageCanvasContext.putImageData(imageData, 0, 0);
+
+ // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
+ // `createImageBitmap` that takes `ImageBitmapOptions`.
+ const imageBitmap = await createImageBitmap(imageCanvas as HTMLCanvasElement, {
+ premultiplyAlpha: 'premultiply',
+ imageOrientation: orientation,
+ });
+
+ const dst = t.device.createTexture({
+ size: dstSize,
+ format: kColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const flipSrcBeforeCopy = orientation === 'flipY';
+ const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: imageData.data,
+ srcOrigin,
+ srcSize,
+ dstOrigin,
+ dstSize,
+ subRectSize: copyExtent,
+ format: kColorFormat,
+ flipSrcBeforeCopy,
+ srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: false,
+ dstPremultiplied,
+ },
+ });
+
+ t.doTestAndCheckResult(
+ { source: imageBitmap, origin: srcOrigin, flipY: srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: dstOrigin,
+ colorSpace: 'srgb',
+ premultipliedAlpha: dstPremultiplied,
+ },
+ texelViewExpected,
+ copyExtent,
+ // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
+ // allow diffs of 1ULP since that's the generally-appropriate threshold.
+ { maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/README.txt
new file mode 100644
index 0000000000..be68b34dd6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/README.txt
@@ -0,0 +1 @@
+Tests for copyToTexture from all possible sources (video, canvas, ImageBitmap, ...)
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts
new file mode 100644
index 0000000000..babecb17fb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/canvas.spec.ts
@@ -0,0 +1,764 @@
+export const description = `
+copyToTexture with HTMLCanvasElement and OffscreenCanvas sources.
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import {
+ kCanvasAlphaModes,
+ kTextureFormatInfo,
+ kValidTextureFormatsForCopyE2T,
+ RegularTextureFormat,
+} from '../../capability_info.js';
+import { CopyToTextureUtils } from '../../util/copy_to_texture.js';
+import { CanvasType, kAllCanvasTypes, createCanvas } from '../../util/create_elements.js';
+import { TexelCompareOptions } from '../../util/texture/texture_ok.js';
+
+class F extends CopyToTextureUtils {
+ init2DCanvasContentWithColorSpace({
+ width,
+ height,
+ colorSpace,
+ }: {
+ width: number;
+ height: number;
+ colorSpace: 'srgb' | 'display-p3';
+ }): {
+ canvas: HTMLCanvasElement | OffscreenCanvas;
+ expectedSourceData: Uint8ClampedArray;
+ } {
+ const canvas = createCanvas(this, 'onscreen', width, height);
+
+ let canvasContext = null;
+ canvasContext = canvas.getContext('2d', { colorSpace });
+
+ if (canvasContext === null) {
+ this.skip('onscreen canvas 2d context not available');
+ }
+
+ if (
+ typeof canvasContext.getContextAttributes === 'undefined' ||
+ typeof canvasContext.getContextAttributes().colorSpace === 'undefined'
+ ) {
+ this.skip('color space attr is not supported for canvas 2d context');
+ }
+
+ const SOURCE_PIXEL_BYTES = 4;
+ const imagePixels = new Uint8ClampedArray(SOURCE_PIXEL_BYTES * width * height);
+
+ const rectWidth = Math.floor(width / 2);
+ const rectHeight = Math.floor(height / 2);
+
+ const alphaValue = 153;
+
+ let pixelStartPos = 0;
+ // Red;
+ for (let i = 0; i < rectHeight; ++i) {
+ for (let j = 0; j < rectWidth; ++j) {
+ pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
+ imagePixels[pixelStartPos] = 255;
+ imagePixels[pixelStartPos + 1] = 0;
+ imagePixels[pixelStartPos + 2] = 0;
+ imagePixels[pixelStartPos + 3] = alphaValue;
+ }
+ }
+
+ // Lime;
+ for (let i = 0; i < rectHeight; ++i) {
+ for (let j = rectWidth; j < width; ++j) {
+ pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
+ imagePixels[pixelStartPos] = 0;
+ imagePixels[pixelStartPos + 1] = 255;
+ imagePixels[pixelStartPos + 2] = 0;
+ imagePixels[pixelStartPos + 3] = alphaValue;
+ }
+ }
+
+ // Blue
+ for (let i = rectHeight; i < height; ++i) {
+ for (let j = 0; j < rectWidth; ++j) {
+ pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
+ imagePixels[pixelStartPos] = 0;
+ imagePixels[pixelStartPos + 1] = 0;
+ imagePixels[pixelStartPos + 2] = 255;
+ imagePixels[pixelStartPos + 3] = alphaValue;
+ }
+ }
+
+ // Fuchsia
+ for (let i = rectHeight; i < height; ++i) {
+ for (let j = rectWidth; j < width; ++j) {
+ pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
+ imagePixels[pixelStartPos] = 255;
+ imagePixels[pixelStartPos + 1] = 0;
+ imagePixels[pixelStartPos + 2] = 255;
+ imagePixels[pixelStartPos + 3] = alphaValue;
+ }
+ }
+
+ const imageData = new ImageData(imagePixels, width, height, { colorSpace });
+ // MAINTENANCE_TODO: Remove as any when tsc support imageData.colorSpace
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ if (typeof (imageData as any).colorSpace === 'undefined') {
+ this.skip('color space attr is not supported for ImageData');
+ }
+
+ const ctx = canvasContext as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ ctx.putImageData(imageData, 0, 0);
+
+ return {
+ canvas,
+ expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
+ };
+ }
+
+ // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
+ init2DCanvasContent({
+ canvasType,
+ width,
+ height,
+ }: {
+ canvasType: CanvasType;
+ width: number;
+ height: number;
+ }): {
+ canvas: HTMLCanvasElement | OffscreenCanvas;
+ expectedSourceData: Uint8ClampedArray;
+ } {
+ const canvas = createCanvas(this, canvasType, width, height);
+
+ let canvasContext = null;
+ canvasContext = canvas.getContext('2d');
+
+ if (canvasContext === null) {
+ this.skip(canvasType + ' canvas 2d context not available');
+ }
+
+ const ctx = canvasContext;
+ this.paint2DCanvas(ctx, width, height, 0.6);
+
+ return {
+ canvas,
+ expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
+ };
+ }
+
+ private paint2DCanvas(
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ width: number,
+ height: number,
+ alphaValue: number
+ ) {
+ const rectWidth = Math.floor(width / 2);
+ const rectHeight = Math.floor(height / 2);
+
+ // Red
+ ctx.fillStyle = `rgba(255, 0, 0, ${alphaValue})`;
+ ctx.fillRect(0, 0, rectWidth, rectHeight);
+ // Lime
+ ctx.fillStyle = `rgba(0, 255, 0, ${alphaValue})`;
+ ctx.fillRect(rectWidth, 0, width - rectWidth, rectHeight);
+ // Blue
+ ctx.fillStyle = `rgba(0, 0, 255, ${alphaValue})`;
+ ctx.fillRect(0, rectHeight, rectWidth, height - rectHeight);
+ // Fuchsia
+ ctx.fillStyle = `rgba(255, 0, 255, ${alphaValue})`;
+ ctx.fillRect(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
+ }
+
+ // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
+ initGLCanvasContent({
+ canvasType,
+ contextName,
+ width,
+ height,
+ premultiplied,
+ }: {
+ canvasType: CanvasType;
+ contextName: 'webgl' | 'webgl2';
+ width: number;
+ height: number;
+ premultiplied: boolean;
+ }): {
+ canvas: HTMLCanvasElement | OffscreenCanvas;
+ expectedSourceData: Uint8ClampedArray;
+ } {
+ const canvas = createCanvas(this, canvasType, width, height);
+
+ // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
+ // `OffscreenCanvas.getContext` that takes `string` or a union of context types.
+ const gl = (canvas as HTMLCanvasElement).getContext(contextName, {
+ premultipliedAlpha: premultiplied,
+ }) as WebGLRenderingContext | WebGL2RenderingContext | null;
+
+ if (gl === null) {
+ this.skip(canvasType + ' canvas ' + contextName + ' context not available');
+ }
+ this.trackForCleanup(gl);
+
+ const rectWidth = Math.floor(width / 2);
+ const rectHeight = Math.floor(height / 2);
+
+ const alphaValue = 0.6;
+ const colorValue = premultiplied ? alphaValue : 1.0;
+
+ // For webgl/webgl2 context canvas, if the context created with premultipliedAlpha attributes,
+ // it means that the value in drawing buffer is premultiplied or not. So we should set
+ // premultipliedAlpha value for premultipliedAlpha true gl context and unpremultipliedAlpha value
+ // for the premultipliedAlpha false gl context.
+ gl.enable(gl.SCISSOR_TEST);
+ gl.scissor(0, 0, rectWidth, rectHeight);
+ gl.clearColor(colorValue, 0.0, 0.0, alphaValue);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(rectWidth, 0, width - rectWidth, rectHeight);
+ gl.clearColor(0.0, colorValue, 0.0, alphaValue);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(0, rectHeight, rectWidth, height - rectHeight);
+ gl.clearColor(0.0, 0.0, colorValue, alphaValue);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.scissor(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
+ gl.clearColor(colorValue, colorValue, colorValue, alphaValue);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ return {
+ canvas,
+ expectedSourceData: this.getExpectedReadbackForWebGLCanvas(gl, width, height),
+ };
+ }
+
+ private getDataToInitSourceWebGPUCanvas(
+ width: number,
+ height: number,
+ alphaMode: GPUCanvasAlphaMode
+ ): Uint8ClampedArray {
+ const rectWidth = Math.floor(width / 2);
+ const rectHeight = Math.floor(height / 2);
+
+ const alphaValue = 153;
+ // Always output [153, 153, 153, 153]. When the alphaMode is...
+ // - premultiplied: the readback is CSS `rgba(255, 255, 255, 60%)`.
+ // - opaque: the readback is CSS `rgba(153, 153, 153, 100%)`.
+ // getExpectedReadbackForWebGPUCanvas matches this.
+ const colorValue = alphaValue;
+
+ // BGRA8Unorm texture
+ const initialData = new Uint8ClampedArray(4 * width * height);
+ const maxRectHeightIndex = width * rectHeight;
+ for (let pixelIndex = 0; pixelIndex < initialData.length / 4; ++pixelIndex) {
+ const index = pixelIndex * 4;
+
+ // Top-half two rectangles
+ if (pixelIndex < maxRectHeightIndex) {
+ // top-left side rectangle
+ if (pixelIndex % width < rectWidth) {
+ // top-left side rectangle
+ initialData[index] = colorValue;
+ initialData[index + 1] = 0;
+ initialData[index + 2] = 0;
+ initialData[index + 3] = alphaValue;
+ } else {
+ // top-right side rectangle
+ initialData[index] = 0;
+ initialData[index + 1] = colorValue;
+ initialData[index + 2] = 0;
+ initialData[index + 3] = alphaValue;
+ }
+ } else {
+ // Bottom-half two rectangles
+ // bottom-left side rectangle
+ if (pixelIndex % width < rectWidth) {
+ initialData[index] = 0;
+ initialData[index + 1] = 0;
+ initialData[index + 2] = colorValue;
+ initialData[index + 3] = alphaValue;
+ } else {
+ // bottom-right side rectangle
+ initialData[index] = colorValue;
+ initialData[index + 1] = colorValue;
+ initialData[index + 2] = colorValue;
+ initialData[index + 3] = alphaValue;
+ }
+ }
+ }
+ return initialData;
+ }
+
+ initSourceWebGPUCanvas({
+ device,
+ canvasType,
+ width,
+ height,
+ alphaMode,
+ }: {
+ device: GPUDevice;
+ canvasType: CanvasType;
+ width: number;
+ height: number;
+ alphaMode: GPUCanvasAlphaMode;
+ }): {
+ canvas: HTMLCanvasElement | OffscreenCanvas;
+ expectedSourceData: Uint8ClampedArray;
+ } {
+ const canvas = createCanvas(this, canvasType, width, height);
+
+ const gpuContext = canvas.getContext('webgpu');
+
+ if (!(gpuContext instanceof GPUCanvasContext)) {
+ this.skip(canvasType + ' canvas webgpu context not available');
+ }
+
+ gpuContext.configure({
+ device,
+ format: 'bgra8unorm',
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
+ alphaMode,
+ });
+
+ // BGRA8Unorm texture
+ const initialData = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);
+ const canvasTexture = gpuContext.getCurrentTexture();
+ device.queue.writeTexture(
+ { texture: canvasTexture },
+ initialData,
+ {
+ bytesPerRow: width * 4,
+ rowsPerImage: height,
+ },
+ {
+ width,
+ height,
+ depthOrArrayLayers: 1,
+ }
+ );
+
+ return {
+ canvas,
+ expectedSourceData: this.getExpectedReadbackForWebGPUCanvas(width, height, alphaMode),
+ };
+ }
+
+ private getExpectedReadbackFor2DCanvas(
+ context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+ width: number,
+ height: number
+ ): Uint8ClampedArray {
+ // Always read back the raw data from canvas
+ return context.getImageData(0, 0, width, height).data;
+ }
+
+ private getExpectedReadbackForWebGLCanvas(
+ gl: WebGLRenderingContext | WebGL2RenderingContext,
+ width: number,
+ height: number
+ ): Uint8ClampedArray {
+ const bytesPerPixel = 4;
+
+ const sourcePixels = new Uint8ClampedArray(width * height * bytesPerPixel);
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, sourcePixels);
+
+ return this.doFlipY(sourcePixels, width, height, bytesPerPixel);
+ }
+
+ private getExpectedReadbackForWebGPUCanvas(
+ width: number,
+ height: number,
+ alphaMode: GPUCanvasAlphaMode
+ ): Uint8ClampedArray {
+ const bytesPerPixel = 4;
+
+ const rgbaPixels = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);
+
+ // The source canvas has bgra8unorm back resource. We
+ // swizzle the channels to align with 2d/webgl canvas and
+ // clear alpha to 255 (1.0) when context alphaMode
+ // is set to opaque (follow webgpu spec).
+ for (let i = 0; i < height; ++i) {
+ for (let j = 0; j < width; ++j) {
+ const pixelPos = i * width + j;
+ const r = rgbaPixels[pixelPos * bytesPerPixel + 2];
+ if (alphaMode === 'opaque') {
+ rgbaPixels[pixelPos * bytesPerPixel + 3] = 255;
+ }
+
+ rgbaPixels[pixelPos * bytesPerPixel + 2] = rgbaPixels[pixelPos * bytesPerPixel];
+ rgbaPixels[pixelPos * bytesPerPixel] = r;
+ }
+ }
+
+ return rgbaPixels;
+ }
+
+ doCopyContentsTest(
+ source: HTMLCanvasElement | OffscreenCanvas,
+ expectedSourceImage: Uint8ClampedArray,
+ p: {
+ width: number;
+ height: number;
+ dstColorFormat: RegularTextureFormat;
+ srcDoFlipYDuringCopy: boolean;
+ srcPremultiplied: boolean;
+ dstPremultiplied: boolean;
+ }
+ ) {
+ const dst = this.device.createTexture({
+ size: {
+ width: p.width,
+ height: p.height,
+ depthOrArrayLayers: 1,
+ },
+ format: p.dstColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ // Construct expected value for different dst color format
+ const info = kTextureFormatInfo[p.dstColorFormat];
+ const expFormat = info.baseFormat ?? p.dstColorFormat;
+
+ // For 2d canvas, get expected pixels with getImageData(), which returns unpremultiplied
+ // values.
+ const expectedDestinationImage = this.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: expectedSourceImage,
+ srcOrigin: [0, 0],
+ srcSize: [p.width, p.height],
+ dstOrigin: [0, 0],
+ dstSize: [p.width, p.height],
+ subRectSize: [p.width, p.height],
+ format: expFormat,
+ flipSrcBeforeCopy: false,
+ srcDoFlipYDuringCopy: p.srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: p.srcPremultiplied,
+ dstPremultiplied: p.dstPremultiplied,
+ },
+ });
+
+ this.doTestAndCheckResult(
+ { source, origin: { x: 0, y: 0 }, flipY: p.srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: { x: 0, y: 0 },
+ colorSpace: 'srgb',
+ premultipliedAlpha: p.dstPremultiplied,
+ },
+ expectedDestinationImage,
+ { width: p.width, height: p.height, depthOrArrayLayers: 1 },
+ // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
+ // allow diffs of 1ULP since that's the generally-appropriate threshold.
+ { maxDiffULPsForNormFormat: 1, maxDiffULPsForFloatFormat: 1 }
+ );
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('copy_contents_from_2d_context_canvas')
+ .desc(
+ `
+ Test HTMLCanvasElement and OffscreenCanvas with 2d context
+ can be copied to WebGPU texture correctly.
+
+ It creates HTMLCanvasElement/OffscreenCanvas with '2d'.
+ Use fillRect(2d context) to render red rect for top-left,
+ green rect for top-right, blue rect for bottom-left and white for bottom-right.
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the canvas contents.
+
+ Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and unpremultiplied input if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ The tests covers:
+ - Valid canvas type
+ - Valid 2d context type
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+ - TODO(#913): color space tests need to be added
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ .combine('dstAlphaMode', kCanvasAlphaModes)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15])
+ .combine('height', [1, 2, 4, 15])
+ )
+ .fn(async t => {
+ const { width, height, canvasType, dstAlphaMode } = t.params;
+
+ const { canvas, expectedSourceData } = t.init2DCanvasContent({
+ canvasType,
+ width,
+ height,
+ });
+
+ t.doCopyContentsTest(canvas, expectedSourceData, {
+ srcPremultiplied: false,
+ dstPremultiplied: dstAlphaMode === 'premultiplied',
+ ...t.params,
+ });
+ });
+
+g.test('copy_contents_from_gl_context_canvas')
+ .desc(
+ `
+ Test HTMLCanvasElement and OffscreenCanvas with webgl/webgl2 context
+ can be copied to WebGPU texture correctly.
+
+ It creates HTMLCanvasElement/OffscreenCanvas with webgl'/'webgl2'.
+ Use scissor + clear to render red rect for top-left, green rect
+ for top-right, blue rect for bottom-left and white for bottom-right.
+ And do premultiply alpha in advance if the webgl/webgl2 context is created
+ with premultipliedAlpha : true.
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the canvas contents.
+
+ Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and unpremultiplied input if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ The tests covers:
+ - Valid canvas type
+ - Valid webgl/webgl2 context type
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage'(named 'srcDoFlipYDuringCopy' in cases)
+ - TODO: color space tests need to be added
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('contextName', ['webgl', 'webgl2'] as const)
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ .combine('srcPremultiplied', [true, false])
+ .combine('dstAlphaMode', kCanvasAlphaModes)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15])
+ .combine('height', [1, 2, 4, 15])
+ )
+ .fn(async t => {
+ const { width, height, canvasType, contextName, srcPremultiplied, dstAlphaMode } = t.params;
+
+ const { canvas, expectedSourceData } = t.initGLCanvasContent({
+ canvasType,
+ contextName,
+ width,
+ height,
+ premultiplied: srcPremultiplied,
+ });
+
+ t.doCopyContentsTest(canvas, expectedSourceData, {
+ dstPremultiplied: dstAlphaMode === 'premultiplied',
+ ...t.params,
+ });
+ });
+
+g.test('copy_contents_from_gpu_context_canvas')
+ .desc(
+ `
+ Test HTMLCanvasElement and OffscreenCanvas with webgpu context
+ can be copied to WebGPU texture correctly.
+
+ It creates HTMLCanvasElement/OffscreenCanvas with 'webgpu'.
+ Use writeTexture to copy pixels to back buffer. The results are:
+ red rect for top-left, green rect for top-right, blue rect for bottom-left
+ and white for bottom-right.
+
+ TODO: Actually test alphaMode = opaque.
+ And do premultiply alpha in advance if the webgpu context is created
+ with alphaMode="premultiplied".
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the canvas contents.
+
+ Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and unpremultiplied input if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ The tests covers:
+ - Valid canvas type
+ - Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - TODO: test more source image alphaMode
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage'(named 'srcDoFlipYDuringCopy' in cases)
+ - TODO: color space tests need to be added
+
+ And the expected results are all passed.
+ `
+ )
+ .params(u =>
+ u
+ .combine('canvasType', kAllCanvasTypes)
+ .combine('srcAndDstInSameGPUDevice', [true, false])
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ // .combine('srcAlphaMode', kCanvasAlphaModes)
+ .combine('srcAlphaMode', ['premultiplied'] as const)
+ .combine('dstAlphaMode', kCanvasAlphaModes)
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15])
+ .combine('height', [1, 2, 4, 15])
+ )
+ .beforeAllSubcases(t => {
+ t.selectMismatchedDeviceOrSkipTestCase(undefined);
+ })
+ .fn(async t => {
+ const {
+ width,
+ height,
+ canvasType,
+ srcAndDstInSameGPUDevice,
+ srcAlphaMode,
+ dstAlphaMode,
+ } = t.params;
+
+ const device = srcAndDstInSameGPUDevice ? t.device : t.mismatchedDevice;
+ const { canvas: source, expectedSourceData } = t.initSourceWebGPUCanvas({
+ device,
+ canvasType,
+ width,
+ height,
+ alphaMode: srcAlphaMode,
+ });
+
+ t.doCopyContentsTest(source, expectedSourceData, {
+ srcPremultiplied: srcAlphaMode === 'premultiplied',
+ dstPremultiplied: dstAlphaMode === 'premultiplied',
+ ...t.params,
+ });
+ });
+
+g.test('color_space_conversion')
+ .desc(
+ `
+ Test HTMLCanvasElement with 2d context can created with 'colorSpace' attribute.
+ Using CopyExternalImageToTexture to copy from such type of canvas needs
+ to do color space converting correctly.
+
+ It creates HTMLCanvasElement/OffscreenCanvas with '2d' and 'colorSpace' attributes.
+ Use fillRect(2d context) to render red rect for top-left,
+ green rect for top-right, blue rect for bottom-left and white for bottom-right.
+
+ Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
+ of dst texture, and read the contents out to compare with the canvas contents.
+
+ Provide premultiplied input if 'premultipliedAlpha' in 'GPUImageCopyTextureTagged'
+ is set to 'true' and unpremultiplied input if it is set to 'false'.
+
+ If 'flipY' in 'GPUImageCopyExternalImage' is set to 'true', copy will ensure the result
+ is flipped.
+
+ If color space from source input and user defined dstTexture color space are different, the
+ result must convert the content to user defined color space
+
+ The tests covers:
+ - Valid dstColorFormat of copyExternalImageToTexture()
+ - Valid dest alphaMode
+ - Valid 'flipY' config in 'GPUImageCopyExternalImage' (named 'srcDoFlipYDuringCopy' in cases)
+ - Valid 'colorSpace' config in 'dstColorSpace'
+
+ And the expected results are all passed.
+
+ TODO: Enhance test data with colors that aren't always opaque and fully saturated.
+ TODO: Consider refactoring src data setup with TexelView.writeTextureData.
+ `
+ )
+ .params(u =>
+ u
+ .combine('srcColorSpace', ['srgb', 'display-p3'] as const)
+ .combine('dstColorSpace', ['srgb'] as const)
+ .combine('dstColorFormat', kValidTextureFormatsForCopyE2T)
+ .combine('dstPremultiplied', [true, false])
+ .combine('srcDoFlipYDuringCopy', [true, false])
+ .beginSubcases()
+ .combine('width', [1, 2, 4, 15, 255, 256])
+ .combine('height', [1, 2, 4, 15, 255, 256])
+ )
+ .fn(async t => {
+ const {
+ width,
+ height,
+ srcColorSpace,
+ dstColorSpace,
+ dstColorFormat,
+ dstPremultiplied,
+ srcDoFlipYDuringCopy,
+ } = t.params;
+ const { canvas, expectedSourceData } = t.init2DCanvasContentWithColorSpace({
+ width,
+ height,
+ colorSpace: srcColorSpace,
+ });
+
+ const dst = t.device.createTexture({
+ size: { width, height },
+ format: dstColorFormat,
+ usage:
+ GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const expectedDestinationImage = t.getExpectedDstPixelsFromSrcPixels({
+ srcPixels: expectedSourceData,
+ srcOrigin: [0, 0],
+ srcSize: [width, height],
+ dstOrigin: [0, 0],
+ dstSize: [width, height],
+ subRectSize: [width, height],
+ // copyExternalImageToTexture does not perform gamma-encoding into `-srgb` formats.
+ format: kTextureFormatInfo[dstColorFormat].baseFormat ?? dstColorFormat,
+ flipSrcBeforeCopy: false,
+ srcDoFlipYDuringCopy,
+ conversion: {
+ srcPremultiplied: false,
+ dstPremultiplied,
+ srcColorSpace,
+ dstColorSpace,
+ },
+ });
+
+ const texelCompareOptions: TexelCompareOptions = {
+ maxFractionalDiff: 0,
+ maxDiffULPsForNormFormat: 1,
+ };
+ if (srcColorSpace !== dstColorSpace) {
+ // Color space conversion seems prone to errors up to about 0.0003 on f32, 0.0007 on f16.
+ texelCompareOptions.maxFractionalDiff = 0.001;
+ } else {
+ texelCompareOptions.maxDiffULPsForFloatFormat = 1;
+ }
+
+ t.doTestAndCheckResult(
+ { source: canvas, origin: { x: 0, y: 0 }, flipY: srcDoFlipYDuringCopy },
+ {
+ texture: dst,
+ origin: { x: 0, y: 0 },
+ colorSpace: dstColorSpace,
+ premultipliedAlpha: dstPremultiplied,
+ },
+ expectedDestinationImage,
+ { width, height, depthOrArrayLayers: 1 },
+ texelCompareOptions
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts
new file mode 100644
index 0000000000..fde4827d77
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/copyToTexture/video.spec.ts
@@ -0,0 +1,15 @@
+export const description = `
+copyToTexture with HTMLVideoElement (and other video-type sources?).
+
+- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), color spaces
+ (bt.601, bt.709, bt.2020)
+- TODO: enhance with more cases with crop, rotation, etc.
+
+TODO: consider whether external_texture and copyToTexture video tests should be in the same file
+TODO: plan
+`;
+
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { GPUTest } from '../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/README.txt
new file mode 100644
index 0000000000..c0ec3fb745
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/README.txt
@@ -0,0 +1 @@
+Tests for external textures.
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts
new file mode 100644
index 0000000000..ea45a986e4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/external_texture/video.spec.ts
@@ -0,0 +1,439 @@
+export const description = `
+Tests for external textures from HTMLVideoElement (and other video-type sources?).
+
+- videos with various encodings/formats (webm vp8, webm vp9, ogg theora, mp4), color spaces
+ (bt.601, bt.709, bt.2020)
+- TODO: enhance with more cases with crop, rotation, etc.
+
+TODO: consider whether external_texture and copyToTexture video tests should be in the same file
+`;
+
+import { getResourcePath } from '../../../common/framework/resources.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { makeTable, valueof } from '../../../common/util/data_tables.js';
+import { GPUTest } from '../../gpu_test.js';
+import {
+ startPlayingAndWaitForVideo,
+ getVideoColorSpaceInit,
+ getVideoFrameFromVideoElement,
+ waitForNextFrame,
+} from '../../web_platform/util.js';
+
+const kHeight = 16;
+const kWidth = 16;
+const kFormat = 'rgba8unorm';
+
+const kVideoInfo = /* prettier-ignore */ makeTable(
+ ['colorSpace', 'mimeType'] as const,
+ [ undefined, undefined] as const, {
+ // All video names
+ 'red-green.webmvp8.webm' : [ 'REC601', 'video/webm; codecs=vp8'],
+ 'red-green.theora.ogv' : [ 'REC601', 'video/ogg; codecs=theora'],
+ 'red-green.mp4' : [ 'REC601', 'video/mp4; codecs=avc1.4d400c'],
+ 'red-green.bt601.vp9.webm' : [ 'REC601', 'video/webm; codecs=vp9'],
+ 'red-green.bt709.vp9.webm' : [ 'REC709', 'video/webm; codecs=vp9'],
+ 'red-green.bt2020.vp9.webm' : [ 'REC2020', 'video/webm; codecs=vp9']
+} as const);
+type VideoName = keyof typeof kVideoInfo;
+type VideoInfo = valueof<typeof kVideoInfo>;
+
+const kVideoExpectations = [
+ {
+ videoName: 'red-green.webmvp8.webm',
+ _redExpectation: new Uint8Array([0xf8, 0x24, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x3f, 0xfb, 0x00, 0xff]),
+ },
+ {
+ videoName: 'red-green.theora.ogv',
+ _redExpectation: new Uint8Array([0xf8, 0x24, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x3f, 0xfb, 0x00, 0xff]),
+ },
+ {
+ videoName: 'red-green.mp4',
+ _redExpectation: new Uint8Array([0xf8, 0x24, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x3f, 0xfb, 0x00, 0xff]),
+ },
+ {
+ videoName: 'red-green.bt601.vp9.webm',
+ _redExpectation: new Uint8Array([0xf8, 0x24, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x3f, 0xfb, 0x00, 0xff]),
+ },
+ {
+ videoName: 'red-green.bt709.vp9.webm',
+ _redExpectation: new Uint8Array([0xff, 0x00, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x00, 0xff, 0x00, 0xff]),
+ },
+ {
+ videoName: 'red-green.bt2020.vp9.webm',
+ _redExpectation: new Uint8Array([0xff, 0x00, 0x00, 0xff]),
+ _greenExpectation: new Uint8Array([0x00, 0xff, 0x00, 0xff]),
+ },
+] as const;
+
+export const g = makeTestGroup(GPUTest);
+
+function createExternalTextureSamplingTestPipeline(t: GPUTest): GPURenderPipeline {
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+ @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec4<f32>, 6>(
+ vec4<f32>( 1.0, 1.0, 0.0, 1.0),
+ vec4<f32>( 1.0, -1.0, 0.0, 1.0),
+ vec4<f32>(-1.0, -1.0, 0.0, 1.0),
+ vec4<f32>( 1.0, 1.0, 0.0, 1.0),
+ vec4<f32>(-1.0, -1.0, 0.0, 1.0),
+ vec4<f32>(-1.0, 1.0, 0.0, 1.0)
+ );
+ return pos[VertexIndex];
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var s : sampler;
+ @group(0) @binding(1) var t : texture_external;
+
+ @fragment fn main(@builtin(position) FragCoord : vec4<f32>)
+ -> @location(0) vec4<f32> {
+ return textureSampleBaseClampToEdge(t, s, FragCoord.xy / vec2<f32>(16.0, 16.0));
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ format: kFormat,
+ },
+ ],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ return pipeline;
+}
+
+function createExternalTextureSamplingTestBindGroup(
+ t: GPUTest,
+ source: HTMLVideoElement | VideoFrame,
+ pipeline: GPURenderPipeline
+): GPUBindGroup {
+ const linearSampler = t.device.createSampler();
+
+ const externalTexture = t.device.importExternalTexture({
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ source: source as any,
+ });
+
+ const bindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: linearSampler,
+ },
+ {
+ binding: 1,
+ resource: externalTexture,
+ },
+ ],
+ });
+
+ return bindGroup;
+}
+
+function getVideoElementAndInfo(
+ t: GPUTest,
+ sourceType: 'VideoElement' | 'VideoFrame',
+ videoName: VideoName
+): { videoElement: HTMLVideoElement; videoInfo: VideoInfo } {
+ if (sourceType === 'VideoFrame' && typeof VideoFrame === 'undefined') {
+ t.skip('WebCodec is not supported');
+ }
+
+ const videoElement = document.createElement('video');
+ const videoInfo = kVideoInfo[videoName];
+
+ if (videoElement.canPlayType(videoInfo.mimeType) === '') {
+ t.skip('Video codec is not supported');
+ }
+
+ const videoUrl = getResourcePath(videoName);
+ videoElement.src = videoUrl;
+
+ return { videoElement, videoInfo };
+}
+
+g.test('importExternalTexture,sample')
+ .desc(
+ `
+Tests that we can import an HTMLVideoElement/VideoFrame into a GPUExternalTexture, sample from it
+for several combinations of video format and color space.
+`
+ )
+ .params(u =>
+ u //
+ .combine('sourceType', ['VideoElement', 'VideoFrame'] as const)
+ .combineWithParams(kVideoExpectations)
+ )
+ .fn(async t => {
+ const sourceType = t.params.sourceType;
+ const { videoElement, videoInfo } = getVideoElementAndInfo(t, sourceType, t.params.videoName);
+
+ await startPlayingAndWaitForVideo(videoElement, async () => {
+ const source =
+ sourceType === 'VideoFrame'
+ ? await getVideoFrameFromVideoElement(
+ t,
+ videoElement,
+ getVideoColorSpaceInit(videoInfo.colorSpace)
+ )
+ : videoElement;
+
+ const colorAttachment = t.device.createTexture({
+ format: kFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const pipeline = createExternalTextureSamplingTestPipeline(t);
+ const bindGroup = createExternalTextureSamplingTestBindGroup(t, source, pipeline);
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ passEncoder.setPipeline(pipeline);
+ passEncoder.setBindGroup(0, bindGroup);
+ passEncoder.draw(6);
+ passEncoder.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+
+ // Top left corner should be red. Sample a few pixels away from the edges to avoid compression
+ // artifacts.
+ t.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ kFormat,
+ { x: 5, y: 5 },
+ {
+ exp: t.params._redExpectation,
+ }
+ );
+
+ // Bottom right corner should be green. Sample a few pixels away from the edges to avoid
+ // compression artifacts.
+ t.expectSinglePixelIn2DTexture(
+ colorAttachment,
+ kFormat,
+ { x: kWidth - 5, y: kHeight - 5 },
+ {
+ exp: t.params._greenExpectation,
+ }
+ );
+
+ if (sourceType === 'VideoFrame') (source as VideoFrame).close();
+ });
+ });
+
+g.test('importExternalTexture,expired')
+ .desc(
+ `
+Tests that GPUExternalTexture.expired is false when HTMLVideoElement is not updated
+or VideoFrame(webcodec) is alive. And it will be changed to true when imported
+HTMLVideoElement is updated or imported VideoFrame is closed. Using expired
+GPUExternalTexture results in an error.
+
+TODO: Make this test work without requestVideoFrameCallback support (in waitForNextFrame).
+`
+ )
+ .params(u =>
+ u //
+ .combine('sourceType', ['VideoElement', 'VideoFrame'] as const)
+ )
+ .fn(async t => {
+ const sourceType = t.params.sourceType;
+ const { videoElement } = getVideoElementAndInfo(t, sourceType, 'red-green.webmvp8.webm');
+
+ if (!('requestVideoFrameCallback' in videoElement)) {
+ t.skip('HTMLVideoElement.requestVideoFrameCallback is not supported');
+ }
+
+ const colorAttachment = t.device.createTexture({
+ format: kFormat,
+ size: { width: kWidth, height: kHeight, depthOrArrayLayers: 1 },
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ const passDescriptor = {
+ colorAttachments: [
+ {
+ view: colorAttachment.createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ } as const;
+
+ const bindGroupLayout = t.device.createBindGroupLayout({
+ entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} }],
+ });
+
+ let bindGroup: GPUBindGroup;
+ const useExternalTexture = () => {
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(passDescriptor);
+ passEncoder.setBindGroup(0, bindGroup);
+ passEncoder.end();
+ return commandEncoder.finish();
+ };
+
+ let externalTexture: GPUExternalTexture;
+ await startPlayingAndWaitForVideo(videoElement, async () => {
+ const source =
+ sourceType === 'VideoFrame'
+ ? await getVideoFrameFromVideoElement(t, videoElement)
+ : videoElement;
+ externalTexture = t.device.importExternalTexture({
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ source: source as any,
+ });
+ // Set `bindGroup` here, which will then be used in microtask1 and microtask3.
+ bindGroup = t.device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [{ binding: 0, resource: externalTexture }],
+ });
+
+ const commandBuffer = useExternalTexture();
+ t.expectGPUError('validation', () => t.device.queue.submit([commandBuffer]), false);
+ t.expect(!externalTexture.expired);
+
+ if (sourceType === 'VideoFrame') {
+ (source as VideoFrame).close();
+ const commandBuffer = useExternalTexture();
+ t.expectGPUError('validation', () => t.device.queue.submit([commandBuffer]), true);
+ t.expect(externalTexture.expired);
+ }
+ });
+
+ if (sourceType === 'VideoElement') {
+ // Update new video frame.
+ await waitForNextFrame(videoElement, () => {
+ // VideoFrame is updated. GPUExternalTexture imported from HTMLVideoElement should be expired.
+ // Using the GPUExternalTexture should result in an error.
+ const commandBuffer = useExternalTexture();
+ t.expectGPUError('validation', () => t.device.queue.submit([commandBuffer]), true);
+ t.expect(externalTexture.expired);
+ });
+ }
+ });
+
+g.test('importExternalTexture,compute')
+ .desc(
+ `
+Tests that we can import an HTMLVideoElement/VideoFrame into a GPUExternalTexture and use it in a
+compute shader, for several combinations of video format and color space.
+`
+ )
+ .params(u =>
+ u //
+ .combine('sourceType', ['VideoElement', 'VideoFrame'] as const)
+ .combineWithParams(kVideoExpectations)
+ )
+ .fn(async t => {
+ const sourceType = t.params.sourceType;
+ const { videoElement, videoInfo } = getVideoElementAndInfo(t, sourceType, t.params.videoName);
+
+ await startPlayingAndWaitForVideo(videoElement, async () => {
+ const source =
+ sourceType === 'VideoFrame'
+ ? await getVideoFrameFromVideoElement(
+ t,
+ videoElement,
+ getVideoColorSpaceInit(videoInfo.colorSpace)
+ )
+ : videoElement;
+ const externalTexture = t.device.importExternalTexture({
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ source: source as any,
+ });
+
+ const outputTexture = t.device.createTexture({
+ format: 'rgba8unorm',
+ size: [2, 1, 1],
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ // Shader will load a pixel near the upper left and lower right corners, which are then
+ // stored in storage texture.
+ module: t.device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var t : texture_external;
+ @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>;
+
+ @compute @workgroup_size(1) fn main() {
+ var red : vec4<f32> = textureLoad(t, vec2<i32>(10,10));
+ textureStore(outImage, vec2<i32>(0, 0), red);
+ var green : vec4<f32> = textureLoad(t, vec2<i32>(70,118));
+ textureStore(outImage, vec2<i32>(1, 0), green);
+ return;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [
+ { binding: 0, resource: externalTexture },
+ { binding: 1, resource: outputTexture.createView() },
+ ],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+
+ // Pixel loaded from top left corner should be red.
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ kFormat,
+ { x: 0, y: 0 },
+ {
+ exp: t.params._redExpectation,
+ }
+ );
+
+ // Pixel loaded from Bottom right corner should be green.
+ t.expectSinglePixelIn2DTexture(
+ outputTexture,
+ kFormat,
+ { x: 1, y: 0 },
+ {
+ exp: t.params._greenExpectation,
+ }
+ );
+
+ if (sourceType === 'VideoFrame') (source as VideoFrame).close();
+ });
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/README.txt b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/README.txt
new file mode 100644
index 0000000000..9f623b1434
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/README.txt
@@ -0,0 +1,17 @@
+Reference tests (reftests) for WebGPU canvas presentation.
+
+These render some contents to a canvas using WebGPU, and WPT compares the rendering result with
+the "reference" versions (in `ref/`) which render with 2D canvas.
+
+This tests things like:
+- The canvas has the correct orientation.
+- The canvas renders with the correct transfer function.
+- The canvas blends and interpolates in the correct color encoding.
+
+TODO(#918): Test all possible color spaces (once we have more than 1)
+TODO(#921): Why is there sometimes a difference of 1 (e.g. 3f vs 40) in canvas_size_different_with_back_buffer_size?
+And why does chromium's image_diff show diffs on other pixels that don't seem to have diffs?
+TODO(#1093): Test rgba16float values which are out of gamut of the canvas but under SDR luminance.
+TODO(#1093): Test rgba16float values which are above SDR luminance.
+TODO(#1116): Test canvas scaling.
+TODO: Test transferControlToOffscreen, used from {the same,another} thread
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.html.ts
new file mode 100644
index 0000000000..c4ffef79a4
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.html.ts
@@ -0,0 +1,34 @@
+import { runRefTest } from './gpu_ref_test.js';
+
+runRefTest(async t => {
+ function draw(canvasId: string, format: GPUTextureFormat) {
+ const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
+
+ const ctx = (canvas.getContext('webgpu') as unknown) as GPUCanvasContext;
+ ctx.configure({
+ device: t.device,
+ format,
+ });
+
+ const colorAttachment = ctx.getCurrentTexture();
+ const colorAttachmentView = colorAttachment.createView();
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorAttachmentView,
+ clearValue: { r: 0.4, g: 1.0, b: 0.0, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ draw('cvs0', 'bgra8unorm');
+ draw('cvs1', 'rgba8unorm');
+ draw('cvs2', 'rgba16float');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.https.html
new file mode 100644
index 0000000000..3639d3ca82
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_clear.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_clear</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU cleared canvas should be presented correctly" />
+ <link rel="match" href="./ref/canvas_clear-ref.html" />
+ <canvas id="cvs0" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs1" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs2" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module" src="canvas_clear.html.js"></script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace.html.ts
new file mode 100644
index 0000000000..0b3dd41bcb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace.html.ts
@@ -0,0 +1,82 @@
+import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
+import { Float16Array } from '../../../external/petamoriken/float16/float16.js';
+import { kCanvasAlphaModes, kCanvasColorSpaces } from '../../capability_info.js';
+
+import { runRefTest } from './gpu_ref_test.js';
+
+function bgra8UnormFromRgba8Unorm(rgba8Unorm: Uint8Array) {
+ const bgra8Unorm = rgba8Unorm.slice();
+ for (let i = 0; i < bgra8Unorm.length; i += 4) {
+ [bgra8Unorm[i], bgra8Unorm[i + 2]] = [bgra8Unorm[i + 2], bgra8Unorm[i]];
+ }
+ return bgra8Unorm;
+}
+
+function rgba16floatFromRgba8unorm(rgba8Unorm: Uint8Array) {
+ const rgba16Float = new Float16Array(rgba8Unorm.length);
+ for (let i = 0; i < rgba8Unorm.length; ++i) {
+ rgba16Float[i] = rgba8Unorm[i] / 255;
+ }
+ return rgba16Float;
+}
+
+export function runColorSpaceTest(format: GPUTextureFormat) {
+ runRefTest(async t => {
+ const device = t.device;
+
+ // prettier-ignore
+ const kRGBA8UnormData = new Uint8Array([
+ 0, 255, 0, 255,
+ 117, 251, 7, 255,
+ 170, 35, 209, 255,
+ 80, 150, 200, 255,
+ ]);
+ const kBGRA8UnormData = bgra8UnormFromRgba8Unorm(kRGBA8UnormData);
+ const kRGBA16FloatData = rgba16floatFromRgba8unorm(kRGBA8UnormData);
+ const width = kRGBA8UnormData.length / 4;
+
+ const testData: { [id: string]: Uint8Array | Float16Array } = {
+ rgba8unorm: kRGBA8UnormData,
+ bgra8unorm: kBGRA8UnormData,
+ rgba16float: kRGBA16FloatData,
+ };
+
+ function createCanvas(
+ alphaMode: GPUCanvasAlphaMode,
+ format: GPUTextureFormat,
+ colorSpace: PredefinedColorSpace
+ ) {
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = 1;
+ const context = canvas.getContext('webgpu') as GPUCanvasContext;
+ context.configure({
+ device,
+ format,
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ alphaMode,
+ colorSpace,
+ });
+
+ const textureData = testData[format];
+ const texture = context.getCurrentTexture();
+ device.queue.writeTexture(
+ { texture },
+ textureData.buffer,
+ {
+ bytesPerRow: textureData.byteLength,
+ },
+ { width: 4, height: 1 }
+ );
+ document.body.appendChild(canvas);
+ }
+
+ const u = kUnitCaseParamsBuilder
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('colorSpace', kCanvasColorSpaces);
+
+ for (const { alphaMode, colorSpace } of u) {
+ createCanvas(alphaMode, format, colorSpace);
+ }
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html
new file mode 100644
index 0000000000..c910c97b1d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_colorspace_bgra8unorm</title>
+ <meta charset="utf-8" />
+ <style>
+ canvas {
+ width: 128px;
+ height: 128px;
+ margin-right: 5px;
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+ }
+ </style>
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU bgra8norm canvas with colorSpace set should be rendered correctly" />
+ <link rel="match" href="./ref/canvas_colorspace-ref.html" />
+ <script type="module">
+ import { runColorSpaceTest } from './canvas_colorspace.html.js';
+ runColorSpaceTest('bgra8unorm');
+ </script>
+ <body></body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html
new file mode 100644
index 0000000000..7f57858e49
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_colorspace_rgba16float</title>
+ <meta charset="utf-8" />
+ <style>
+ canvas {
+ width: 128px;
+ height: 128px;
+ margin-right: 5px;
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+ }
+ </style>
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU rgba16float canvas with colorSpace set should be rendered correctly" />
+ <link rel="match" href="./ref/canvas_colorspace-ref.html" />
+ <meta name=fuzzy content="maxDifference=1;totalPixels=8192">
+ <script type="module">
+ import { runColorSpaceTest } from './canvas_colorspace.html.js';
+ runColorSpaceTest('rgba16float');
+ </script>
+ <body></body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html
new file mode 100644
index 0000000000..e57e04ef5c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_colorspace_rgba8unorm</title>
+ <meta charset="utf-8" />
+ <style>
+ canvas {
+ width: 128px;
+ height: 128px;
+ margin-right: 5px;
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+ }
+ </style>
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU rgba8unorm canvas with colorSpace set should be rendered correctly" />
+ <link rel="match" href="./ref/canvas_colorspace-ref.html" />
+ <script type="module">
+ import { runColorSpaceTest } from './canvas_colorspace.html.js';
+ runColorSpaceTest('rgba8unorm');
+ </script>
+ <body></body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex.html.ts
new file mode 100644
index 0000000000..cee1698b69
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex.html.ts
@@ -0,0 +1,771 @@
+import { assert, unreachable } from '../../../common/util/util.js';
+import { kTextureFormatInfo } from '../../capability_info.js';
+import { gammaDecompress, float32ToFloat16Bits } from '../../util/conversion.js';
+import { align } from '../../util/math.js';
+
+import { runRefTest } from './gpu_ref_test.js';
+
+type WriteCanvasMethod =
+ | 'copyBufferToTexture'
+ | 'copyTextureToTexture'
+ | 'copyExternalImageToTexture'
+ | 'DrawTextureSample'
+ | 'DrawVertexColor'
+ | 'DrawFragcoord'
+ | 'FragmentTextureStore'
+ | 'ComputeWorkgroup1x1TextureStore'
+ | 'ComputeWorkgroup16x16TextureStore';
+
+export function run(
+ format: GPUTextureFormat,
+ targets: { cvs: HTMLCanvasElement; writeCanvasMethod: WriteCanvasMethod }[]
+) {
+ runRefTest(async t => {
+ let shaderValue: number = 0x66 / 0xff;
+ let isOutputSrgb = false;
+ switch (format) {
+ case 'bgra8unorm':
+ case 'rgba8unorm':
+ case 'rgba16float':
+ break;
+ case 'bgra8unorm-srgb':
+ case 'rgba8unorm-srgb':
+ // NOTE: "-srgb" cases haven't been tested (there aren't any .html files that use them).
+
+ // Reverse gammaCompress to get same value shader output as non-srgb formats:
+ shaderValue = gammaDecompress(shaderValue);
+ isOutputSrgb = true;
+ break;
+ default:
+ unreachable();
+ }
+ const shaderValueStr = shaderValue.toFixed(5);
+
+ function copyBufferToTexture(ctx: GPUCanvasContext) {
+ const rows = ctx.canvas.height;
+ const bytesPerPixel = kTextureFormatInfo[format].bytesPerBlock;
+ if (bytesPerPixel === undefined) {
+ unreachable();
+ }
+ const bytesPerRow = align(bytesPerPixel * ctx.canvas.width, 256);
+ const componentsPerPixel = 4;
+
+ const buffer = t.device.createBuffer({
+ mappedAtCreation: true,
+ size: rows * bytesPerRow,
+ usage: GPUBufferUsage.COPY_SRC,
+ });
+ let red: Uint8Array | Uint16Array;
+ let green: Uint8Array | Uint16Array;
+ let blue: Uint8Array | Uint16Array;
+ let yellow: Uint8Array | Uint16Array;
+
+ const mapping = buffer.getMappedRange();
+ let data: Uint8Array | Uint16Array;
+ switch (format) {
+ case 'bgra8unorm':
+ case 'bgra8unorm-srgb':
+ {
+ data = new Uint8Array(mapping);
+ red = new Uint8Array([0x00, 0x00, 0x66, 0xff]);
+ green = new Uint8Array([0x00, 0x66, 0x00, 0xff]);
+ blue = new Uint8Array([0x66, 0x00, 0x00, 0xff]);
+ yellow = new Uint8Array([0x00, 0x66, 0x66, 0xff]);
+ }
+ break;
+ case 'rgba8unorm':
+ case 'rgba8unorm-srgb':
+ {
+ data = new Uint8Array(mapping);
+ red = new Uint8Array([0x66, 0x00, 0x00, 0xff]);
+ green = new Uint8Array([0x00, 0x66, 0x00, 0xff]);
+ blue = new Uint8Array([0x00, 0x00, 0x66, 0xff]);
+ yellow = new Uint8Array([0x66, 0x66, 0x00, 0xff]);
+ }
+ break;
+ case 'rgba16float':
+ {
+ data = new Uint16Array(mapping);
+ red = new Uint16Array([
+ float32ToFloat16Bits(0.4),
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(1.0),
+ ]);
+ green = new Uint16Array([
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(0.4),
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(1.0),
+ ]);
+ blue = new Uint16Array([
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(0.4),
+ float32ToFloat16Bits(1.0),
+ ]);
+ yellow = new Uint16Array([
+ float32ToFloat16Bits(0.4),
+ float32ToFloat16Bits(0.4),
+ float32ToFloat16Bits(0.0),
+ float32ToFloat16Bits(1.0),
+ ]);
+ }
+ break;
+ default:
+ unreachable();
+ }
+ for (let i = 0; i < ctx.canvas.width; ++i)
+ for (let j = 0; j < ctx.canvas.height; ++j) {
+ let pixel: Uint8Array | Uint16Array;
+ if (i < ctx.canvas.width / 2) {
+ if (j < ctx.canvas.height / 2) {
+ pixel = red;
+ } else {
+ pixel = blue;
+ }
+ } else {
+ if (j < ctx.canvas.height / 2) {
+ pixel = green;
+ } else {
+ pixel = yellow;
+ }
+ }
+ data.set(pixel, (i + j * (bytesPerRow / bytesPerPixel)) * componentsPerPixel);
+ }
+ buffer.unmap();
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyBufferToTexture({ buffer, bytesPerRow }, { texture: ctx.getCurrentTexture() }, [
+ ctx.canvas.width,
+ ctx.canvas.height,
+ 1,
+ ]);
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ function getImageBitmap(ctx: GPUCanvasContext): Promise<ImageBitmap> {
+ const data = new Uint8ClampedArray(ctx.canvas.width * ctx.canvas.height * 4);
+ for (let i = 0; i < ctx.canvas.width; ++i)
+ for (let j = 0; j < ctx.canvas.height; ++j) {
+ const offset = (i + j * ctx.canvas.width) * 4;
+ if (i < ctx.canvas.width / 2) {
+ if (j < ctx.canvas.height / 2) {
+ data.set([0x66, 0x00, 0x00, 0xff], offset);
+ } else {
+ data.set([0x00, 0x00, 0x66, 0xff], offset);
+ }
+ } else {
+ if (j < ctx.canvas.height / 2) {
+ data.set([0x00, 0x66, 0x00, 0xff], offset);
+ } else {
+ data.set([0x66, 0x66, 0x00, 0xff], offset);
+ }
+ }
+ }
+ const imageData = new ImageData(data, ctx.canvas.width, ctx.canvas.height);
+ return createImageBitmap(imageData);
+ }
+
+ function setupSrcTexture(imageBitmap: ImageBitmap): GPUTexture {
+ const [srcWidth, srcHeight] = [imageBitmap.width, imageBitmap.height];
+ const srcTexture = t.device.createTexture({
+ size: [srcWidth, srcHeight, 1],
+ format,
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.RENDER_ATTACHMENT |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.COPY_SRC,
+ });
+ t.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture: srcTexture }, [
+ imageBitmap.width,
+ imageBitmap.height,
+ ]);
+ return srcTexture;
+ }
+
+ async function copyExternalImageToTexture(ctx: GPUCanvasContext) {
+ const imageBitmap = await getImageBitmap(ctx);
+ t.device.queue.copyExternalImageToTexture(
+ { source: imageBitmap },
+ { texture: ctx.getCurrentTexture() },
+ [imageBitmap.width, imageBitmap.height]
+ );
+ }
+
+ async function copyTextureToTexture(ctx: GPUCanvasContext) {
+ const imageBitmap = await getImageBitmap(ctx);
+ const srcTexture = setupSrcTexture(imageBitmap);
+
+ const encoder = t.device.createCommandEncoder();
+ encoder.copyTextureToTexture(
+ { texture: srcTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ { texture: ctx.getCurrentTexture(), mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
+ [imageBitmap.width, imageBitmap.height, 1]
+ );
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ async function DrawTextureSample(ctx: GPUCanvasContext) {
+ const imageBitmap = await getImageBitmap(ctx);
+ const srcTexture = setupSrcTexture(imageBitmap);
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) fragUV : vec2<f32>,
+}
+
+@vertex
+fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0));
+
+ var uv = array<vec2<f32>, 6>(
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(0.0, 0.0));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ output.fragUV = uv[VertexIndex];
+ return output;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ // NOTE: "-srgb" cases haven't been tested (there aren't any .html files that use them).
+ code: `
+@group(0) @binding(0) var mySampler: sampler;
+@group(0) @binding(1) var myTexture: texture_2d<f32>;
+
+fn gammaDecompress(n: f32) -> f32 {
+ var r = n;
+ if (r <= 0.04045) {
+ r = r * 25.0 / 323.0;
+ } else {
+ r = pow((200.0 * r + 11.0) / 121.0, 12.0 / 5.0);
+ }
+ r = clamp(r, 0.0, 1.0);
+ return r;
+}
+
+@fragment
+fn srgbMain(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
+ var result = textureSample(myTexture, mySampler, fragUV);
+ result.r = gammaDecompress(result.r);
+ result.g = gammaDecompress(result.g);
+ result.b = gammaDecompress(result.b);
+ return result;
+}
+
+@fragment
+fn linearMain(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
+ return textureSample(myTexture, mySampler, fragUV);
+}
+ `,
+ }),
+ entryPoint: isOutputSrgb ? 'srgbMain' : 'linearMain',
+ targets: [{ format }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ const sampler = t.device.createSampler({
+ magFilter: 'nearest',
+ minFilter: 'nearest',
+ });
+
+ const uniformBindGroup = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: sampler,
+ },
+ {
+ binding: 1,
+ resource: srcTexture.createView(),
+ },
+ ],
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: ctx.getCurrentTexture().createView(),
+
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.setBindGroup(0, uniformBindGroup);
+ passEncoder.draw(6, 1, 0, 0);
+ passEncoder.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+ }
+
+ function DrawVertexColor(ctx: GPUCanvasContext) {
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) fragColor : vec4<f32>,
+}
+
+@vertex
+fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 0.5, 0.5),
+ vec2<f32>( 0.5, -0.5),
+ vec2<f32>(-0.5, -0.5),
+ vec2<f32>( 0.5, 0.5),
+ vec2<f32>(-0.5, -0.5),
+ vec2<f32>(-0.5, 0.5));
+
+ var offset = array<vec2<f32>, 4>(
+ vec2<f32>( -0.5, 0.5),
+ vec2<f32>( 0.5, 0.5),
+ vec2<f32>(-0.5, -0.5),
+ vec2<f32>( 0.5, -0.5));
+
+ var color = array<vec4<f32>, 4>(
+ vec4<f32>(${shaderValueStr}, 0.0, 0.0, 1.0),
+ vec4<f32>(0.0, ${shaderValueStr}, 0.0, 1.0),
+ vec4<f32>(0.0, 0.0, ${shaderValueStr}, 1.0),
+ vec4<f32>(${shaderValueStr}, ${shaderValueStr}, 0.0, 1.0));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex % 6u] + offset[VertexIndex / 6u], 0.0, 1.0);
+ output.fragColor = color[VertexIndex / 6u];
+ return output;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+@fragment
+fn main(@location(0) fragColor: vec4<f32>) -> @location(0) vec4<f32> {
+ return fragColor;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: ctx.getCurrentTexture().createView(),
+
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.draw(24, 1, 0, 0);
+ passEncoder.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+ }
+
+ function DrawFragcoord(ctx: GPUCanvasContext) {
+ const halfCanvasWidthStr = (ctx.canvas.width / 2).toFixed();
+ const halfCanvasHeightStr = (ctx.canvas.height / 2).toFixed();
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>
+}
+
+@vertex
+fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ return output;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+@group(0) @binding(0) var mySampler: sampler;
+@group(0) @binding(1) var myTexture: texture_2d<f32>;
+
+@fragment
+fn main(@builtin(position) fragcoord: vec4<f32>) -> @location(0) vec4<f32> {
+ var coord = vec2<u32>(floor(fragcoord.xy));
+ var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ if (coord.x < ${halfCanvasWidthStr}u) {
+ if (coord.y < ${halfCanvasHeightStr}u) {
+ color.r = ${shaderValueStr};
+ } else {
+ color.b = ${shaderValueStr};
+ }
+ } else {
+ if (coord.y < ${halfCanvasHeightStr}u) {
+ color.g = ${shaderValueStr};
+ } else {
+ color.r = ${shaderValueStr};
+ color.g = ${shaderValueStr};
+ }
+ }
+ return color;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: ctx.getCurrentTexture().createView(),
+
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.draw(6, 1, 0, 0);
+ passEncoder.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+ }
+
+ function FragmentTextureStore(ctx: GPUCanvasContext) {
+ const halfCanvasWidthStr = (ctx.canvas.width / 2).toFixed();
+ const halfCanvasHeightStr = (ctx.canvas.height / 2).toFixed();
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: t.device.createShaderModule({
+ code: `
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>
+}
+
+@vertex
+fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+ var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0));
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+ return output;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: t.device.createShaderModule({
+ code: `
+@group(0) @binding(0) var outImage : texture_storage_2d<${format}, write>;
+
+@fragment
+fn main(@builtin(position) fragcoord: vec4<f32>) -> @location(0) vec4<f32> {
+ var coord = vec2<u32>(floor(fragcoord.xy));
+ var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ if (coord.x < ${halfCanvasWidthStr}u) {
+ if (coord.y < ${halfCanvasHeightStr}u) {
+ color.r = ${shaderValueStr};
+ } else {
+ color.b = ${shaderValueStr};
+ }
+ } else {
+ if (coord.y < ${halfCanvasHeightStr}u) {
+ color.g = ${shaderValueStr};
+ } else {
+ color.r = ${shaderValueStr};
+ color.g = ${shaderValueStr};
+ }
+ }
+ textureStore(outImage, vec2<i32>(coord), color);
+ return color;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ targets: [{ format }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [{ binding: 0, resource: ctx.getCurrentTexture().createView() }],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const outputTexture = t.device.createTexture({
+ format,
+ size: [ctx.canvas.width, ctx.canvas.height, 1],
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: outputTexture.createView(),
+
+ clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.setBindGroup(0, bg);
+ passEncoder.draw(6, 1, 0, 0);
+ passEncoder.end();
+ t.device.queue.submit([commandEncoder.finish()]);
+ }
+
+ function ComputeWorkgroup1x1TextureStore(ctx: GPUCanvasContext) {
+ const halfCanvasWidthStr = (ctx.canvas.width / 2).toFixed();
+ const halfCanvasHeightStr = (ctx.canvas.height / 2).toFixed();
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+@group(0) @binding(0) var outImage : texture_storage_2d<${format}, write>;
+
+@compute @workgroup_size(1, 1, 1)
+fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ if (GlobalInvocationID.x < ${halfCanvasWidthStr}u) {
+ if (GlobalInvocationID.y < ${halfCanvasHeightStr}u) {
+ color.r = ${shaderValueStr};
+ } else {
+ color.b = ${shaderValueStr};
+ }
+ } else {
+ if (GlobalInvocationID.y < ${halfCanvasHeightStr}u) {
+ color.g = ${shaderValueStr};
+ } else {
+ color.r = ${shaderValueStr};
+ color.g = ${shaderValueStr};
+ }
+ }
+ textureStore(outImage, vec2<i32>(GlobalInvocationID.xy), color);
+ return;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [{ binding: 0, resource: ctx.getCurrentTexture().createView() }],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(ctx.canvas.width, ctx.canvas.height, 1);
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ function ComputeWorkgroup16x16TextureStore(ctx: GPUCanvasContext) {
+ const canvasWidthStr = ctx.canvas.width.toFixed();
+ const canvasHeightStr = ctx.canvas.height.toFixed();
+ const halfCanvasWidthStr = (ctx.canvas.width / 2).toFixed();
+ const halfCanvasHeightStr = (ctx.canvas.height / 2).toFixed();
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({
+ code: `
+@group(0) @binding(0) var outImage : texture_storage_2d<${format}, write>;
+
+@compute @workgroup_size(16, 16, 1)
+fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ if (GlobalInvocationID.x >= ${canvasWidthStr}u ||
+ GlobalInvocationID.y >= ${canvasHeightStr}u) {
+ return;
+ }
+ var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ if (GlobalInvocationID.x < ${halfCanvasWidthStr}u) {
+ if (GlobalInvocationID.y < ${halfCanvasHeightStr}u) {
+ color.r = ${shaderValueStr};
+ } else {
+ color.b = ${shaderValueStr};
+ }
+ } else {
+ if (GlobalInvocationID.y < ${halfCanvasHeightStr}u) {
+ color.g = ${shaderValueStr};
+ } else {
+ color.r = ${shaderValueStr};
+ color.g = ${shaderValueStr};
+ }
+ }
+ textureStore(outImage, vec2<i32>(GlobalInvocationID.xy), color);
+ return;
+}
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ entries: [{ binding: 0, resource: ctx.getCurrentTexture().createView() }],
+ layout: pipeline.getBindGroupLayout(0),
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(
+ align(ctx.canvas.width, 16) / 16,
+ align(ctx.canvas.height, 16) / 16,
+ 1
+ );
+ pass.end();
+ t.device.queue.submit([encoder.finish()]);
+ }
+
+ for (const { cvs, writeCanvasMethod } of targets) {
+ const ctx = cvs.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ let usage: GPUTextureUsageFlags;
+ switch (writeCanvasMethod) {
+ case 'copyBufferToTexture':
+ case 'copyTextureToTexture':
+ usage = GPUTextureUsage.COPY_DST;
+ break;
+ case 'copyExternalImageToTexture':
+ usage = GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
+ break;
+ case 'DrawTextureSample':
+ case 'DrawVertexColor':
+ case 'DrawFragcoord':
+ usage = GPUTextureUsage.RENDER_ATTACHMENT;
+ break;
+ case 'FragmentTextureStore':
+ case 'ComputeWorkgroup1x1TextureStore':
+ case 'ComputeWorkgroup16x16TextureStore':
+ usage = GPUTextureUsage.STORAGE_BINDING;
+ break;
+ default:
+ unreachable();
+ }
+
+ ctx.configure({
+ device: t.device,
+ format,
+ usage,
+ });
+
+ switch (writeCanvasMethod) {
+ case 'copyBufferToTexture':
+ copyBufferToTexture(ctx);
+ break;
+ case 'copyExternalImageToTexture':
+ await copyExternalImageToTexture(ctx);
+ break;
+ case 'copyTextureToTexture':
+ await copyTextureToTexture(ctx);
+ break;
+ case 'DrawTextureSample':
+ await DrawTextureSample(ctx);
+ break;
+ case 'DrawVertexColor':
+ DrawVertexColor(ctx);
+ break;
+ case 'DrawFragcoord':
+ DrawFragcoord(ctx);
+ break;
+ case 'FragmentTextureStore':
+ FragmentTextureStore(ctx);
+ break;
+ case 'ComputeWorkgroup1x1TextureStore':
+ ComputeWorkgroup1x1TextureStore(ctx);
+ break;
+ case 'ComputeWorkgroup16x16TextureStore':
+ ComputeWorkgroup16x16TextureStore(ctx);
+ break;
+ default:
+ unreachable();
+ }
+ }
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_copy.https.html
new file mode 100644
index 0000000000..d378bdfcf5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_copy.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_bgra8unorm_copy</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_copy_buffer_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_texture_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_external_image_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('bgra8unorm', [
+ { cvs: cvs_copy_buffer_to_texture, writeCanvasMethod: 'copyBufferToTexture' },
+ { cvs: cvs_copy_texture_to_texture, writeCanvasMethod: 'copyTextureToTexture' },
+ { cvs: cvs_copy_external_image_to_texture, writeCanvasMethod: 'copyExternalImageToTexture' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_draw.https.html
new file mode 100644
index 0000000000..99049e6e32
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_bgra8unorm_draw.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_bgra8unorm_draw</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_draw_texture_sample" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_vertex_color" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_fragcoord" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('bgra8unorm', [
+ { cvs: cvs_draw_texture_sample, writeCanvasMethod: 'DrawTextureSample' },
+ { cvs: cvs_draw_vertex_color, writeCanvasMethod: 'DrawVertexColor' },
+ { cvs: cvs_draw_fragcoord, writeCanvasMethod: 'DrawFragcoord' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_copy.https.html
new file mode 100644
index 0000000000..400afa121b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_copy.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba16float_copy</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_copy_buffer_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_texture_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_external_image_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba16float', [
+ { cvs: cvs_copy_buffer_to_texture, writeCanvasMethod: 'copyBufferToTexture' },
+ { cvs: cvs_copy_texture_to_texture, writeCanvasMethod: 'copyTextureToTexture' },
+ { cvs: cvs_copy_external_image_to_texture, writeCanvasMethod: 'copyExternalImageToTexture' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_draw.https.html
new file mode 100644
index 0000000000..a647fc2956
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_draw.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba16float_draw</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_draw_texture_sample" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_vertex_color" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_fragcoord" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba16float', [
+ { cvs: cvs_draw_texture_sample, writeCanvasMethod: 'DrawTextureSample' },
+ { cvs: cvs_draw_vertex_color, writeCanvasMethod: 'DrawVertexColor' },
+ { cvs: cvs_draw_fragcoord, writeCanvasMethod: 'DrawFragcoord' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_store.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_store.https.html
new file mode 100644
index 0000000000..b812129b0b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba16float_store.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba16float_store</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_fragment_texture_store" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_compute_texture_store_1" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_compute_texture_store_2" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba16float', [
+ { cvs: cvs_fragment_texture_store, writeCanvasMethod: 'FragmentTextureStore' },
+ { cvs: cvs_compute_texture_store_1, writeCanvasMethod: 'ComputeWorkgroup1x1TextureStore' },
+ { cvs: cvs_compute_texture_store_2, writeCanvasMethod: 'ComputeWorkgroup16x16TextureStore' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_copy.https.html
new file mode 100644
index 0000000000..d2570a3bdf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_copy.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba8unorm_copy</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_copy_buffer_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_texture_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_copy_external_image_to_texture" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba8unorm', [
+ { cvs: cvs_copy_buffer_to_texture, writeCanvasMethod: 'copyBufferToTexture' },
+ { cvs: cvs_copy_texture_to_texture, writeCanvasMethod: 'copyTextureToTexture' },
+ { cvs: cvs_copy_external_image_to_texture, writeCanvasMethod: 'copyExternalImageToTexture' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_draw.https.html
new file mode 100644
index 0000000000..647a829259
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_draw.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba8unorm_draw</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_draw_texture_sample" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_vertex_color" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_draw_fragcoord" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba8unorm', [
+ { cvs: cvs_draw_texture_sample, writeCanvasMethod: 'DrawTextureSample' },
+ { cvs: cvs_draw_vertex_color, writeCanvasMethod: 'DrawVertexColor' },
+ { cvs: cvs_draw_fragcoord, writeCanvasMethod: 'DrawFragcoord' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_store.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_store.https.html
new file mode 100644
index 0000000000..b82745658e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_complex_rgba8unorm_store.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_complex_rgba8unorm_store</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_complex-ref.html" />
+
+ <canvas id="cvs_fragment_texture_store" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_compute_texture_store_1" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs_compute_texture_store_2" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+
+ <script type="module">
+ import { run } from './canvas_complex.html.js';
+ run('rgba8unorm', [
+ { cvs: cvs_fragment_texture_store, writeCanvasMethod: 'FragmentTextureStore' },
+ { cvs: cvs_compute_texture_store_1, writeCanvasMethod: 'ComputeWorkgroup1x1TextureStore' },
+ { cvs: cvs_compute_texture_store_2, writeCanvasMethod: 'ComputeWorkgroup16x16TextureStore' },
+ ]);
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha.html.ts
new file mode 100644
index 0000000000..5819ca5d77
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha.html.ts
@@ -0,0 +1,177 @@
+import { assert, unreachable } from '../../../common/util/util.js';
+
+import { runRefTest } from './gpu_ref_test.js';
+
+type WriteCanvasMethod = 'draw' | 'copy';
+
+export function run(
+ format: GPUTextureFormat,
+ alphaMode: GPUCanvasAlphaMode,
+ writeCanvasMethod: WriteCanvasMethod
+) {
+ runRefTest(async t => {
+ const module = t.device.createShaderModule({
+ code: `
+struct VertexOutput {
+@builtin(position) Position : vec4<f32>,
+@location(0) fragColor : vec4<f32>,
+}
+
+@vertex
+fn mainVS(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
+var pos = array<vec2<f32>, 6>(
+ vec2<f32>( 0.75, 0.75),
+ vec2<f32>( 0.75, -0.75),
+ vec2<f32>(-0.75, -0.75),
+ vec2<f32>( 0.75, 0.75),
+ vec2<f32>(-0.75, -0.75),
+ vec2<f32>(-0.75, 0.75));
+
+var offset = array<vec2<f32>, 4>(
+vec2<f32>( -0.25, 0.25),
+vec2<f32>( 0.25, 0.25),
+vec2<f32>(-0.25, -0.25),
+vec2<f32>( 0.25, -0.25));
+
+// Alpha channel value is set to 0.5 regardless of the canvas alpha mode.
+// For 'opaque' mode, it shouldn't affect the end result, as the alpha channel should always get cleared to 1.0.
+var color = array<vec4<f32>, 4>(
+ vec4<f32>(0.4, 0.0, 0.0, 0.5),
+ vec4<f32>(0.0, 0.4, 0.0, 0.5),
+ vec4<f32>(0.0, 0.0, 0.4, 0.5),
+ vec4<f32>(0.4, 0.4, 0.0, 0.5)); // 0.4 -> 0x66
+
+var output : VertexOutput;
+output.Position = vec4<f32>(pos[VertexIndex % 6u] + offset[VertexIndex / 6u], 0.0, 1.0);
+output.fragColor = color[VertexIndex / 6u];
+return output;
+}
+
+@fragment
+fn mainFS(@location(0) fragColor: vec4<f32>) -> @location(0) vec4<f32> {
+return fragColor;
+}
+ `,
+ });
+
+ document.querySelectorAll('canvas').forEach(canvas => {
+ const ctx = canvas.getContext('webgpu');
+ assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');
+
+ switch (format) {
+ case 'bgra8unorm':
+ case 'bgra8unorm-srgb':
+ case 'rgba8unorm':
+ case 'rgba8unorm-srgb':
+ case 'rgba16float':
+ break;
+ default:
+ unreachable();
+ }
+
+ let usage = 0;
+ switch (writeCanvasMethod) {
+ case 'draw':
+ usage = GPUTextureUsage.RENDER_ATTACHMENT;
+ break;
+ case 'copy':
+ usage = GPUTextureUsage.COPY_DST;
+ break;
+ }
+ ctx.configure({
+ device: t.device,
+ format,
+ usage,
+ alphaMode,
+ });
+
+ // The blending behavior here is to mimic 2d context blending behavior
+ // of drawing rects in order
+ // https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_srcover
+ const kBlendStateSourceOver = {
+ color: {
+ srcFactor: 'src-alpha',
+ dstFactor: 'one-minus-src-alpha',
+ operation: 'add',
+ },
+ alpha: {
+ srcFactor: 'one',
+ dstFactor: 'one-minus-src-alpha',
+ operation: 'add',
+ },
+ } as const;
+
+ const pipeline = t.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'mainVS',
+ },
+ fragment: {
+ module,
+ entryPoint: 'mainFS',
+ targets: [
+ {
+ format,
+ blend: { premultiplied: kBlendStateSourceOver, opaque: undefined }[alphaMode],
+ },
+ ],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ let renderTarget: GPUTexture;
+ switch (writeCanvasMethod) {
+ case 'draw':
+ renderTarget = ctx.getCurrentTexture();
+ break;
+ case 'copy':
+ renderTarget = t.device.createTexture({
+ size: [ctx.canvas.width, ctx.canvas.height],
+ format,
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+ break;
+ }
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: renderTarget.createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.draw(6, 1, 0, 0);
+ passEncoder.draw(6, 1, 6, 0);
+ passEncoder.draw(6, 1, 12, 0);
+ passEncoder.draw(6, 1, 18, 0);
+ passEncoder.end();
+
+ switch (writeCanvasMethod) {
+ case 'draw':
+ break;
+ case 'copy':
+ commandEncoder.copyTextureToTexture(
+ {
+ texture: renderTarget,
+ },
+ {
+ texture: ctx.getCurrentTexture(),
+ },
+ [ctx.canvas.width, ctx.canvas.height]
+ );
+ break;
+ }
+
+ t.device.queue.submit([commandEncoder.finish()]);
+ });
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_copy.https.html
new file mode 100644
index 0000000000..60e8417c16
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_copy.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_bgra8unorm_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('bgra8unorm', 'opaque', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_draw.https.html
new file mode 100644
index 0000000000..c0280a2a99
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_opaque_draw.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_bgra8unorm_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('bgra8unorm', 'opaque', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html
new file mode 100644
index 0000000000..70920dc0e6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_bgra8unorm_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('bgra8unorm', 'premultiplied', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html
new file mode 100644
index 0000000000..d12751fac2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_bgra8unorm_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('bgra8unorm', 'premultiplied', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_copy.https.html
new file mode 100644
index 0000000000..4471f08480
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_copy.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba16float_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba16float', 'opaque', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_draw.https.html
new file mode 100644
index 0000000000..11f0e73ec2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_opaque_draw.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba16float_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba16float', 'opaque', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html
new file mode 100644
index 0000000000..ed722013c1
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba16float_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba16float', 'premultiplied', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html
new file mode 100644
index 0000000000..8a028b168e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba16float_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba16float', 'premultiplied', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_copy.https.html
new file mode 100644
index 0000000000..7147631d19
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_copy.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba8unorm_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba8unorm', 'opaque', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_draw.https.html
new file mode 100644
index 0000000000..ec2bb05ed3
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_opaque_draw.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba8unorm_opaque</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_opaque-ref.html" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba8unorm', 'opaque', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html
new file mode 100644
index 0000000000..fa938aba41
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba8unorm_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba8unorm', 'premultiplied', 'copy');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html
new file mode 100644
index 0000000000..b62e71054c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_composite_alpha_rgba8unorm_premultiplied</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta
+ name="assert"
+ content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
+ />
+ <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
+ <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script type="module">
+ import { run } from './canvas_composite_alpha.html.js';
+ run('rgba8unorm', 'premultiplied', 'draw');
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.html.ts
new file mode 100644
index 0000000000..5eda39268e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.html.ts
@@ -0,0 +1,79 @@
+import { runRefTest } from './gpu_ref_test.js';
+
+runRefTest(async t => {
+ const device = t.device;
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+
+ const module = device.createShaderModule({
+ code: `
+ @vertex fn vs(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2(-1.0, 3.0),
+ vec2(-1.0,-1.0),
+ vec2( 3.0,-1.0)
+ );
+
+ return vec4(pos[VertexIndex], 0.0, 1.0);
+ }
+
+ @fragment fn fs(
+ @builtin(position) Pos : vec4<f32>
+ ) -> @location(0) vec4<f32> {
+ let black = vec4f(0, 0, 0, 1);
+ let white = vec4f(1, 1, 1, 1);
+ let iPos = vec4u(Pos);
+ let check = (iPos.x + iPos.y) & 1;
+ return mix(black, white, f32(check));
+ }
+ `,
+ });
+
+ const pipeline = device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vs',
+ },
+ fragment: {
+ module,
+ entryPoint: 'fs',
+ targets: [{ format: presentationFormat }],
+ },
+ });
+
+ function draw(selector: string, alphaMode: GPUCanvasAlphaMode) {
+ const canvas = document.querySelector(selector) as HTMLCanvasElement;
+ const context = canvas.getContext('webgpu') as GPUCanvasContext;
+ context.configure({
+ device,
+ format: presentationFormat,
+ alphaMode,
+ });
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: context.getCurrentTexture().createView(),
+ clearValue: [0.0, 0.0, 0.0, 0.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.draw(3);
+ pass.end();
+
+ device.queue.submit([encoder.finish()]);
+ }
+
+ draw('#elem1', 'premultiplied');
+ draw('#elem2', 'premultiplied');
+ draw('#elem3', 'premultiplied');
+ draw('#elem4', 'opaque');
+ draw('#elem5', 'opaque');
+ draw('#elem6', 'opaque');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html
new file mode 100644
index 0000000000..f51145645b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/canvas_image_rendering.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_image_rendering</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU canvas with image-rendering set should be rendered correctly" />
+ <link rel="match" href="./ref/canvas_image_rendering-ref.html" />
+ <canvas id="elem1" width="64" height="64" style="width: 99px; height: 99px;"></canvas>
+ <canvas id="elem2" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"></canvas>
+ <canvas id="elem3" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"></canvas>
+ <canvas id="elem4" width="64" height="64" style="width: 99px; height: 99px;"></canvas>
+ <canvas id="elem5" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;"></canvas>
+ <canvas id="elem6" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges"></canvas>
+ <script type="module" src="canvas_image_rendering.html.js"></script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/create-pattern-data-url.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/create-pattern-data-url.ts
new file mode 100644
index 0000000000..aa96bbd85b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/create-pattern-data-url.ts
@@ -0,0 +1,23 @@
+// creates a 4x4 pattern
+export default function createPatternDataURL() {
+ const patternSize = 4;
+ const ctx = document.createElement('canvas').getContext('2d')!;
+ ctx.canvas.width = patternSize;
+ ctx.canvas.height = patternSize;
+
+ const b = [0, 0, 0, 255];
+ const t = [0, 0, 0, 0];
+ const r = [255, 0, 0, 255];
+ const g = [0, 255, 0, 255];
+
+ const imageData = new ImageData(patternSize, patternSize);
+ // prettier-ignore
+ imageData.data.set([
+ b, t, t, r,
+ t, b, g, t,
+ t, r, b, t,
+ g, t, t, b,
+ ].flat());
+ ctx.putImageData(imageData, 0, 0);
+ return { patternSize, imageData, dataURL: ctx.canvas.toDataURL() };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/gpu_ref_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/gpu_ref_test.ts
new file mode 100644
index 0000000000..3b1350b5d9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/gpu_ref_test.ts
@@ -0,0 +1,26 @@
+import { assert } from '../../../common/util/util.js';
+import { takeScreenshotDelayed } from '../../../common/util/wpt_reftest_wait.js';
+
+interface GPURefTest {
+ readonly device: GPUDevice;
+ readonly queue: GPUQueue;
+}
+
+export function runRefTest(fn: (t: GPURefTest) => Promise<void>): void {
+ void (async () => {
+ assert(
+ typeof navigator !== 'undefined' && navigator.gpu !== undefined,
+ 'No WebGPU implementation found'
+ );
+
+ const adapter = await navigator.gpu.requestAdapter();
+ assert(adapter !== null);
+ const device = await adapter.requestDevice();
+ assert(device !== null);
+ const queue = device.queue;
+
+ await fn({ device, queue });
+
+ takeScreenshotDelayed(50);
+ })();
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_clear-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_clear-ref.html
new file mode 100644
index 0000000000..e37b78c3a6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_clear-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU canvas_clear (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <canvas id="cvs0" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs1" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs2" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script>
+ function draw(canvas) {
+ var c = document.getElementById(canvas);
+ var ctx = c.getContext('2d');
+ ctx.fillStyle = '#66FF00';
+ ctx.fillRect(0, 0, c.width, c.height);
+ }
+
+ draw('cvs0');
+ draw('cvs1');
+ draw('cvs2');
+
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html
new file mode 100644
index 0000000000..a6da9f6748
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU canvas_colorspace (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <style>
+ canvas {
+ width: 128px;
+ height: 128px;
+ margin-right: 5px;
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+ }
+ </style>
+ <body></body>
+ <script type="module" src="canvas_colorspace-ref.html.js"></script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html.ts
new file mode 100644
index 0000000000..d6ac122553
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_colorspace-ref.html.ts
@@ -0,0 +1,36 @@
+import { kUnitCaseParamsBuilder } from '../../../../common/framework/params_builder.js';
+import { kCanvasAlphaModes, kCanvasColorSpaces } from '../../../capability_info.js';
+
+// prettier-ignore
+const kRGBAData = new Uint8Array([
+ 0, 255, 0, 255,
+ 117, 251, 7, 255,
+ 170, 35, 209, 255,
+ 80, 150, 200, 255,
+]);
+const width = kRGBAData.length / 4;
+
+function createCanvas(colorSpace: PredefinedColorSpace) {
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = 1;
+ const context = canvas.getContext('2d', {
+ colorSpace,
+ }) as CanvasRenderingContext2D;
+
+ const imgData = context.getImageData(0, 0, width, 1);
+ imgData.data.set(kRGBAData);
+ context.putImageData(imgData, 0, 0);
+
+ document.body.appendChild(canvas);
+}
+
+const u = kUnitCaseParamsBuilder
+ .combine('alphaMode', kCanvasAlphaModes)
+ .combine('colorSpace', kCanvasColorSpaces);
+
+// Generate reference canvases for all combinations from the test.
+// We only need colorSpace to generate the correct reference.
+for (const { colorSpace } of u) {
+ createCanvas(colorSpace);
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_complex-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_complex-ref.html
new file mode 100644
index 0000000000..b1d46c108a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_complex-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU canvas_complex (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <canvas id="cvs0" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs1" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="cvs2" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script>
+ function draw(ctx) {
+ ctx.fillStyle = '#660000';
+ ctx.fillRect(0, 0, 10, 10);
+ ctx.fillStyle = '#006600';
+ ctx.fillRect(10, 0, 10, 10);
+ ctx.fillStyle = '#000066';
+ ctx.fillRect(0, 10, 10, 10);
+ ctx.fillStyle = '#666600';
+ ctx.fillRect(10, 10, 10, 10);
+ }
+
+ draw(cvs0.getContext('2d'));
+ draw(cvs1.getContext('2d'));
+ draw(cvs2.getContext('2d'));
+
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_opaque-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_opaque-ref.html
new file mode 100644
index 0000000000..94b9486514
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_opaque-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU canvas_composite_alpha_premultiplied (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script>
+ document.querySelectorAll('canvas').forEach(canvas => {
+ const ctx = canvas.getContext('2d');
+ ctx.globalAlpha = 1.0;
+ ctx.fillStyle = '#660000';
+ ctx.fillRect(0, 0, 15, 15);
+ ctx.fillStyle = '#006600';
+ ctx.fillRect(5, 0, 15, 15);
+ ctx.fillStyle = '#000066';
+ ctx.fillRect(0, 5, 15, 20);
+ ctx.fillStyle = '#666600';
+ ctx.fillRect(5, 5, 20, 20);
+ });
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_premultiplied-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_premultiplied-ref.html
new file mode 100644
index 0000000000..635625ecc7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_composite_alpha_premultiplied-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU canvas_composite_alpha_premultiplied (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <style>
+ body { background-color: #F0E68C; }
+ #c-canvas { background-color: #8CF0E6; }
+ </style>
+ <canvas id="c-body" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <canvas id="c-canvas" width="20" height="20" style="width: 20px; height: 20px;"></canvas>
+ <script>
+ document.querySelectorAll('canvas').forEach(canvas => {
+ const ctx = canvas.getContext('2d');
+ ctx.globalAlpha = 0.5;
+ ctx.fillStyle = '#660000';
+ ctx.fillRect(0, 0, 15, 15);
+ ctx.fillStyle = '#006600';
+ ctx.fillRect(5, 0, 15, 15);
+ ctx.fillStyle = '#000066';
+ ctx.fillRect(0, 5, 15, 20);
+ ctx.fillStyle = '#666600';
+ ctx.fillRect(5, 5, 20, 20);
+ });
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html
new file mode 100644
index 0000000000..f9eca704e8
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/canvas_image_rendering-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU canvas_image_rendering (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <img id="elem1" width="64" height="64" style="width: 99px; height: 99px;">
+ <img id="elem2" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;">
+ <img id="elem3" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges">
+ <img id="elem4" width="64" height="64" style="width: 99px; height: 99px;">
+ <img id="elem5" width="64" height="64" style="width: 99px; height: 99px; image-rendering: pixelated;">
+ <img id="elem6" width="64" height="64" style="width: 99px; height: 99px; image-rendering: crisp-edges">
+ <script type="module">
+ import { takeScreenshotDelayed } from '../../../../common/util/wpt_reftest_wait.js';
+
+ (async () => {
+ const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKdJREFUeF7t28kJwDAQA0Cp/6I3hJAqNK/kazDr2atJ7u7S9v3Z+76nnz18m7oBboAYIAZMRv//1fMKcAAHkCAJLuYAXoEv8ZMLcAAHcAAHcAAHjBZEOeBSDuAADuAADuAADtjrCqsIqQh98xAkSIIkSIIkSIIkSIKrYzJ6gyRIgiRIgiRIgiRIgiRoZ2hwYcp8gKqwSVF9AXuD9gbtDdobXGWw7nCbB5+MQQlHipKKAAAAAElFTkSuQmCC';
+ await Promise.all([...document.querySelectorAll('img')].map(img => {
+ img.src = dataURL;
+ return img.decode();
+ }));
+
+ takeScreenshotDelayed(50);
+ })();
+ </script>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/resize_observer-ref.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/resize_observer-ref.html
new file mode 100644
index 0000000000..5259a25c27
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/ref/resize_observer-ref.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+ <title>WebGPU ResizeObserver test (ref)</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <style>
+ .outer {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ }
+ .outer>* {
+ display: block;
+ height: 100px;
+ }
+ </style>
+ <body>
+ <div id="dpr"></div>
+ <div class="outer"></div>
+ <script type="module">
+ import { takeScreenshotDelayed } from '../../../../common/util/wpt_reftest_wait.js';
+ import createPatternDataURL from '../create-pattern-data-url.js';
+
+ (async () => {
+ const {patternSize, dataURL} = createPatternDataURL();
+
+ document.querySelector('#dpr').textContent = `dpr: ${devicePixelRatio}`;
+
+ /**
+ * Set the pattern's size on this element so that it draws where
+ * 1 pixel in the pattern maps to 1 devicePixel.
+ */
+ function setPattern(elem) {
+ const oneDevicePixel = 1 / devicePixelRatio;
+ const patternPixels = oneDevicePixel * patternSize;
+ elem.style.backgroundImage = `url("${dataURL}")`;
+ elem.style.backgroundSize = `${patternPixels}px ${patternPixels}px`;
+ }
+
+ /*
+ This ref creates elements like this
+ <body>
+ <div class="outer">
+ <div></div>
+ <div></div>
+ <div></div>
+ ...
+ </div>
+ </body>
+ Where the outer div is a flexbox centering the child elements.
+ Each of the child elements is set to a different width in percent.
+ The devicePixelContentBox size of each child element is observed
+ with a ResizeObserver and when changed, a pattern is applied to
+ the element and the pattern's size set so each pixel in the pattern
+ will be one device pixel.
+ A similar process happens in the test HTML using canvases
+ and patterns generated using putImageData.
+ The test and this reference page should then match.
+ */
+
+ const outerElem = document.querySelector('.outer');
+
+ let resolve;
+ const promise = new Promise(_resolve => (resolve = _resolve));
+
+ /**
+ * Set the pattern's size on this element so that it draws where
+ * 1 pixel in the pattern maps to 1 devicePixel.
+ */
+ function setPatterns(entries) {
+ for (const entry of entries) {
+ setPattern(entry.target)
+ }
+ resolve();
+ }
+
+ const observer = new ResizeObserver(setPatterns);
+ for (let percentSize = 7; percentSize < 100; percentSize += 13) {
+ const innerElem = document.createElement('div');
+ innerElem.style.width = `${percentSize}%`;
+ observer.observe(innerElem, {box:"device-pixel-content-box"});
+ outerElem.appendChild(innerElem);
+ }
+
+ await promise;
+ takeScreenshotDelayed(50);
+ })();
+ </script>
+ </body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.html.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.html.ts
new file mode 100644
index 0000000000..9cb9905a77
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.html.ts
@@ -0,0 +1,150 @@
+import createPatternDataURL from './create-pattern-data-url.js';
+import { runRefTest } from './gpu_ref_test.js';
+
+runRefTest(async t => {
+ const { patternSize, imageData: patternImageData } = createPatternDataURL();
+
+ document.querySelector('#dpr')!.textContent = `dpr: ${devicePixelRatio}`;
+
+ const device = t.device;
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+
+ const module = device.createShaderModule({
+ code: `
+ @vertex fn vs(
+ @builtin(vertex_index) VertexIndex : u32
+ ) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2(-1.0, 3.0),
+ vec2(-1.0,-1.0),
+ vec2( 3.0,-1.0)
+ );
+
+ return vec4(pos[VertexIndex], 0.0, 1.0);
+ }
+
+ @group(0) @binding(0) var pattern: texture_2d<f32>;
+
+ @fragment fn fs(
+ @builtin(position) Pos : vec4<f32>
+ ) -> @location(0) vec4<f32> {
+ let patternSize = textureDimensions(pattern, 0);
+ let uPos = vec2u(Pos.xy) % patternSize;
+ return textureLoad(pattern, uPos, 0);
+ }
+ `,
+ });
+
+ const pipeline = device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module,
+ entryPoint: 'vs',
+ },
+ fragment: {
+ module,
+ entryPoint: 'fs',
+ targets: [{ format: presentationFormat }],
+ },
+ });
+
+ const tex = device.createTexture({
+ size: [patternSize, patternSize, 1],
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
+ });
+ device.queue.writeTexture(
+ { texture: tex },
+ patternImageData.data,
+ { bytesPerRow: patternSize * 4, rowsPerImage: 4 },
+ { width: patternSize, height: patternSize }
+ );
+
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: tex.createView() }],
+ });
+
+ function setCanvasPattern(
+ canvas: HTMLCanvasElement,
+ devicePixelWidth: number,
+ devicePixelHeight: number
+ ) {
+ canvas.width = devicePixelWidth;
+ canvas.height = devicePixelHeight;
+
+ const context = canvas.getContext('webgpu') as GPUCanvasContext;
+ context.configure({
+ device,
+ format: presentationFormat,
+ alphaMode: 'premultiplied',
+ });
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: context.getCurrentTexture().createView(),
+ clearValue: [0.0, 0.0, 0.0, 0.0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(3);
+ pass.end();
+
+ device.queue.submit([encoder.finish()]);
+ }
+
+ /*
+ This test creates elements like this
+ <body>
+ <div class="outer">
+ <canvas></canvas>
+ <canvas></canvas>
+ <canvas></canvas>
+ ...
+ </div>
+ </body>
+ Where the outer div is a flexbox centering the child canvases.
+ Each of the child canvases is set to a different width in percent.
+ The size of each canvas in device pixels is queried with ResizeObserver
+ and then each canvases' resolution is set to that size so that there should
+ be one pixel in each canvas for each device pixel.
+ Each canvas is filled with a pattern using putImageData.
+ In the reference the canvas elements are replaced with divs.
+ For the divs the same pattern is applied with CSS and its size
+ adjusted so the pattern should appear with one pixel in the pattern
+ corresponding to 1 device pixel.
+ The reference and this page should then match.
+ */
+
+ const outerElem = document.querySelector('.outer')!;
+
+ let resolve: (value: unknown) => void;
+ const promise = new Promise(_resolve => (resolve = _resolve));
+
+ function setPatternsUsingSizeInfo(entries: ResizeObserverEntry[]) {
+ for (const entry of entries) {
+ setCanvasPattern(
+ entry.target as HTMLCanvasElement,
+ entry.devicePixelContentBoxSize[0].inlineSize,
+ entry.devicePixelContentBoxSize[0].blockSize
+ );
+ }
+ resolve(true);
+ }
+
+ const observer = new ResizeObserver(setPatternsUsingSizeInfo);
+ for (let percentSize = 7; percentSize < 100; percentSize += 13) {
+ const canvasElem = document.createElement('canvas');
+ canvasElem.style.width = `${percentSize}%`;
+ observer.observe(canvasElem, { box: 'device-pixel-content-box' });
+ outerElem.appendChild(canvasElem);
+ }
+
+ await promise;
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.https.html b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.https.html
new file mode 100644
index 0000000000..2845cc29eb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/reftests/resize_observer.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <title>WebGPU resize_observer</title>
+ <meta charset="utf-8" />
+ <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
+ <meta name="assert" content="WebGPU canvases should return the correct ResizeObserver values" />
+ <link rel="match" href="./ref/resize_observer-ref.html" />
+ <style>
+ .outer {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ }
+ .outer>* {
+ display: block;
+ height: 100px;
+ }
+ </style>
+ <body>
+ <div id="dpr"></div>
+ <div class="outer"></div>
+ <script type="module" src="resize_observer.html.js"></script>
+ </body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts
new file mode 100644
index 0000000000..da986b6c12
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts
@@ -0,0 +1,185 @@
+import { Fixture, SkipTestCase } from '../../common/framework/fixture.js';
+import {
+ assert,
+ ErrorWithExtra,
+ raceWithRejectOnTimeout,
+ unreachable,
+} from '../../common/util/util.js';
+
+declare global {
+ interface HTMLMediaElement {
+ // Add captureStream() support for HTMLMediaElement from
+ // https://w3c.github.io/mediacapture-fromelement/#dom-htmlmediaelement-capturestream
+ captureStream(): MediaStream;
+ }
+}
+
+/**
+ * Starts playing a video and waits for it to be consumable.
+ * Returns a promise which resolves after `callback` (which may be async) completes.
+ *
+ * @param video An HTML5 Video element.
+ * @param callback Function to call when video is ready.
+ *
+ * Adapted from https://github.com/KhronosGroup/WebGL/blob/main/sdk/tests/js/webgl-test-utils.js
+ */
+export function startPlayingAndWaitForVideo(
+ video: HTMLVideoElement,
+ callback: () => unknown | Promise<unknown>
+): Promise<void> {
+ return raceWithRejectOnTimeout(
+ new Promise((resolve, reject) => {
+ const callbackAndResolve = () =>
+ void (async () => {
+ try {
+ await callback();
+ resolve();
+ } catch (ex) {
+ reject();
+ }
+ })();
+ if (video.error) {
+ reject(
+ new ErrorWithExtra('Video.error: ' + video.error.message, () => ({ error: video.error }))
+ );
+ return;
+ }
+
+ video.addEventListener(
+ 'error',
+ event => reject(new ErrorWithExtra('Video received "error" event', () => ({ event }))),
+ true
+ );
+
+ if ('requestVideoFrameCallback' in video) {
+ video.requestVideoFrameCallback(() => {
+ callbackAndResolve();
+ });
+ } else {
+ // If requestVideoFrameCallback isn't available, check each frame if the video has advanced.
+ const timeWatcher = () => {
+ if (video.currentTime > 0) {
+ callbackAndResolve();
+ } else {
+ requestAnimationFrame(timeWatcher);
+ }
+ };
+ timeWatcher();
+ }
+
+ video.loop = true;
+ video.muted = true;
+ video.preload = 'auto';
+ video.play().catch(reject);
+ }),
+ 2000,
+ 'Video never became ready'
+ );
+}
+
+/**
+ * Fire a `callback` when the video reaches a new frame.
+ * Returns a promise which resolves after `callback` (which may be async) completes.
+ *
+ * MAINTENANCE_TODO: Find a way to implement this for browsers without requestVideoFrameCallback as
+ * well, similar to the timeWatcher path in startPlayingAndWaitForVideo. If that path is proven to
+ * work well, we can consider getting rid of the requestVideoFrameCallback path.
+ */
+export function waitForNextFrame(
+ video: HTMLVideoElement,
+ callback: () => unknown | Promise<unknown>
+): Promise<void> {
+ const { promise, callbackAndResolve } = videoCallbackHelper(
+ callback,
+ 'waitForNextFrame timed out'
+ );
+
+ if ('requestVideoFrameCallback' in video) {
+ video.requestVideoFrameCallback(() => {
+ callbackAndResolve();
+ });
+ } else {
+ throw new SkipTestCase('waitForNextFrame currently requires requestVideoFrameCallback');
+ }
+
+ return promise;
+}
+
+type VideoColorSpaceName = 'REC601' | 'REC709' | 'REC2020';
+
+export function getVideoColorSpaceInit(colorSpaceName: VideoColorSpaceName): VideoColorSpaceInit {
+ switch (colorSpaceName) {
+ case 'REC601':
+ return {
+ primaries: 'smpte170m',
+ transfer: 'smpte170m',
+ matrix: 'smpte170m',
+ fullRange: false,
+ };
+ case 'REC709':
+ return { primaries: 'bt709', transfer: 'bt709', matrix: 'bt709', fullRange: false };
+ case 'REC2020':
+ return { primaries: 'bt709', transfer: 'iec61966-2-1', matrix: 'rgb', fullRange: true };
+ default:
+ unreachable();
+ }
+}
+
+export async function getVideoFrameFromVideoElement(
+ test: Fixture,
+ video: HTMLVideoElement,
+ colorSpace: VideoColorSpaceInit = getVideoColorSpaceInit('REC709')
+): Promise<VideoFrame> {
+ if (video.captureStream === undefined) {
+ test.skip('HTMLVideoElement.captureStream is not supported');
+ }
+
+ const track: MediaStreamVideoTrack = video.captureStream().getVideoTracks()[0];
+ const reader = new MediaStreamTrackProcessor({ track }).readable.getReader();
+ const videoFrame = (await reader.read()).value;
+ assert(videoFrame !== undefined, 'unable to get a VideoFrame from track 0');
+ assert(
+ videoFrame.format !== null && videoFrame.timestamp !== null,
+ 'unable to get a valid VideoFrame from track 0'
+ );
+ // Apply color space info because the VideoFrame generated from captured stream
+ // doesn't have it.
+ const bufferSize = videoFrame.allocationSize();
+ const buffer = new ArrayBuffer(bufferSize);
+ const frameLayout = await videoFrame.copyTo(buffer);
+ const frameInit: VideoFrameBufferInit = {
+ format: videoFrame.format,
+ timestamp: videoFrame.timestamp,
+ codedWidth: videoFrame.codedWidth,
+ codedHeight: videoFrame.codedHeight,
+ colorSpace,
+ layout: frameLayout,
+ };
+ return new VideoFrame(buffer, frameInit);
+}
+
+/**
+ * Helper for doing something inside of a (possibly async) callback (directly, not in a following
+ * microtask), and returning a promise when the callback is done.
+ * MAINTENANCE_TODO: Use this in startPlayingAndWaitForVideo (and make sure it works).
+ */
+function videoCallbackHelper(
+ callback: () => unknown | Promise<unknown>,
+ timeoutMessage: string
+): { promise: Promise<void>; callbackAndResolve: () => void } {
+ let callbackAndResolve: () => void;
+
+ const promiseWithoutTimeout = new Promise<void>((resolve, reject) => {
+ callbackAndResolve = () =>
+ void (async () => {
+ try {
+ await callback(); // catches both exceptions and rejections
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ })();
+ });
+ const promise = raceWithRejectOnTimeout(promiseWithoutTimeout, 2000, timeoutMessage);
+ return { promise, callbackAndResolve: callbackAndResolve! };
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts
new file mode 100644
index 0000000000..67f9f693be
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.spec.ts
@@ -0,0 +1,35 @@
+export const description = `
+Tests WebGPU is available in a worker.
+
+Note: The CTS test can be run in a worker by passing in worker=1 as
+a query parameter. This test is specifically to check that WebGPU
+is available in a worker.
+`;
+
+import { Fixture } from '../../../common/framework/fixture.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { assert } from '../../../common/util/util.js';
+
+export const g = makeTestGroup(Fixture);
+
+function isNode(): boolean {
+ return typeof process !== 'undefined' && process?.versions?.node !== undefined;
+}
+
+g.test('worker')
+ .desc(`test WebGPU is available in DedicatedWorkers and check for basic functionality`)
+ .fn(async t => {
+ if (isNode()) {
+ t.skip('node does not support 100% compatible workers');
+ return;
+ }
+ // Note: we load worker_launcher dynamically because ts-node support
+ // is using commonjs which doesn't support import.meta. Further,
+ // we need to put the url in a string add pass the string to import
+ // otherwise typescript tries to parse the file which again, fails.
+ // worker_launcher.js is excluded in node.tsconfig.json.
+ const url = './worker_launcher.js';
+ const { launchWorker } = await import(url);
+ const result = await launchWorker();
+ assert(result.error === undefined, `should be no error from worker but was: ${result.error}`);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts
new file mode 100644
index 0000000000..256a8345ab
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker.ts
@@ -0,0 +1,79 @@
+import { getGPU } from '../../../common/util/navigator_gpu.js';
+import { assert, objectEquals, iterRange } from '../../../common/util/util.js';
+
+async function basicTest() {
+ const adapter = await getGPU().requestAdapter();
+ assert(adapter !== null, 'Failed to get adapter.');
+
+ const device = await adapter.requestDevice();
+ assert(device !== null, 'Failed to get device.');
+
+ const kOffset = 1230000;
+ const pipeline = device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: device.createShaderModule({
+ code: `
+ struct Buffer { data: array<u32>, };
+
+ @group(0) @binding(0) var<storage, read_write> buffer: Buffer;
+ @compute @workgroup_size(1u) fn main(
+ @builtin(global_invocation_id) id: vec3<u32>) {
+ buffer.data[id.x] = id.x + ${kOffset}u;
+ }
+ `,
+ }),
+ entryPoint: 'main',
+ },
+ });
+
+ const kNumElements = 64;
+ const kBufferSize = kNumElements * 4;
+ const buffer = device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const resultBuffer = device.createBuffer({
+ size: kBufferSize,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+
+ const encoder = device.createCommandEncoder();
+
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.dispatchWorkgroups(kNumElements);
+ pass.end();
+
+ encoder.copyBufferToBuffer(buffer, 0, resultBuffer, 0, kBufferSize);
+
+ device.queue.submit([encoder.finish()]);
+
+ const expected = new Uint32Array([...iterRange(kNumElements, x => x + kOffset)]);
+
+ await resultBuffer.mapAsync(GPUMapMode.READ);
+ const actual = new Uint32Array(resultBuffer.getMappedRange());
+
+ assert(objectEquals(actual, expected), 'compute pipeline ran');
+
+ resultBuffer.destroy();
+ buffer.destroy();
+ device.destroy();
+}
+
+self.onmessage = async (ev: MessageEvent) => {
+ let error = undefined;
+ try {
+ await basicTest();
+ } catch (err: unknown) {
+ error = (err as Error).toString();
+ }
+ self.postMessage({ error });
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts
new file mode 100644
index 0000000000..9ae7ee83e2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/worker/worker_launcher.ts
@@ -0,0 +1,16 @@
+export type TestResult = {
+ error: String | undefined;
+};
+
+export async function launchWorker() {
+ const selfPath = import.meta.url;
+ const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
+ const workerPath = selfPathDir + '/worker.js';
+ const worker = new Worker(workerPath, { type: 'module' });
+
+ const promise = new Promise<TestResult>(resolve => {
+ worker.addEventListener('message', ev => resolve(ev.data as TestResult), { once: true });
+ });
+ worker.postMessage({});
+ return await promise;
+}
diff --git a/dom/webgpu/tests/cts/checkout/standalone/index.html b/dom/webgpu/tests/cts/checkout/standalone/index.html
new file mode 100644
index 0000000000..834a0e2179
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/standalone/index.html
@@ -0,0 +1,423 @@
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>WebGPU CTS</title>
+ <link
+ id="favicon"
+ rel="shortcut icon"
+ type="image/png"
+ href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEUAAAAAAAD///+D3c/SAAAAAXRSTlMAQObYZgAAAEpJREFUCB0FwbERgDAMA0BdSkbJQBSuaPABE0WuaKILmpJ/rNVejPKBUXGhqAC5J0gn9ESg2wvdNua8hUoKJQo8b6HyE6a2QHdbP0CPITh2pewWAAAAAElFTkSuQmCC"
+ />
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
+ <meta name="viewport" content="width=device-width" />
+ <!-- Chrome Origin Trial token for https://gpuweb.github.io (see dev_server.ts for localhost tokens) -->
+ <meta http-equiv="origin-trial" content="AmV1vLgjOQ01SlGnVhpoKXy7gLW+K/plXHwHKnYn4S4US98WaSesKBI+XSUMo95unQARyMGDvW70KsfyeYblZQ0AAABQeyJvcmlnaW4iOiJodHRwczovL2dwdXdlYi5naXRodWIuaW86NDQzIiwiZmVhdHVyZSI6IldlYkdQVSIsImV4cGlyeSI6MTY2MzcxODM5OX0=">
+ <link rel="stylesheet" href="third_party/normalize.min.css" />
+ <script src="third_party/jquery/jquery-3.3.1.min.js"></script>
+ <style>
+ :root {
+ color-scheme: light dark;
+
+ --fg-color: #000;
+ --bg-color: #fff;
+ --border-color: #888;
+ --emphasis-fg-color: #F00;
+
+ --results-fg-color: gray;
+ --node-description-fg-color: #gray;
+ --node-hover-bg-color: rgba(0, 0, 0, 0.1);
+
+ --testcaselogbtn-bg-color: #eee;
+ --subtree-border-color: #ddd;
+ --subtree-hover-left-border-color: #000;
+ --multicase-border-color: #55f;
+ --testcase-border-color: #bbf;
+ --testcase-bg-color: #bbb;
+
+ --testcase-data-status-fail-bg-color: #fdd;
+ --testcase-data-status-warn-bg-color: #ffb;
+ --testcase-data-status-pass-bg-color: #cfc;
+ --testcase-data-status-skip-bg-color: #eee;
+
+ --testcase-logs-bg-color: #white;
+ --testcase-log-odd-bg-color: #fff;
+ --testcase-log-even-bg-color: #f8f8f8;
+ --testcase-log-text-fg-color: #666;
+ --testcase-log-text-first-line-fg-color: #000;
+ --button-image-filter: none;
+ }
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --fg-color: #fff;
+ --bg-color: #000;
+ --border-color: #888;
+ --emphasis-fg-color: #F44;
+
+ --results-fg-color: #aaa;
+ --node-description-fg-color: #aaa;
+ --node-hover-bg-color: rgba(255, 255, 255, 0.1);
+
+ --testcaselogbtn-bg-color: #666;
+ --subtree-border-color: #444;
+ --subtree-hover-left-border-color: #FFF;
+ --multicase-border-color: #338;
+ --testcase-border-color: #55a;
+ --testcase-bg-color: #888;
+
+ --testcase-data-status-fail-bg-color: #400;
+ --testcase-data-status-warn-bg-color: #660;
+ --testcase-data-status-pass-bg-color: #040;
+ --testcase-data-status-skip-bg-color: #444;
+
+ --testcase-logs-bg-color: #black;
+ --testcase-log-odd-bg-color: #000;
+ --testcase-log-even-bg-color: #080808;
+ --testcase-log-text-fg-color: #aaa;
+ --testcase-log-text-first-line-fg-color: #fff;
+ --button-image-filter: invert(100%);
+ }
+ }
+ body {
+ font-family: monospace;
+ min-width: 400px;
+ margin: 0.5em;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ h1 {
+ font-size: 1.5em;
+ font-family: 'Poppins', sans-serif;
+ height: 1.2em;
+ vertical-align: middle;
+ }
+ input[type=button],
+ button {
+ cursor: pointer;
+ }
+ .logo {
+ height: 1.2em;
+ float: left;
+ }
+ .important {
+ font-weight: bold;
+ color: var(--emphasis-fg-color);
+ }
+ #options label {
+ display: flex;
+ }
+ table#options {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ #options td {
+ border: 1px solid var(--subtree-border-color);
+ width: 1px; /* to make the columns as small as possible */
+ }
+ #options tr:hover {
+ background: var(--node-hover-bg-color);
+ }
+ #options td:nth-child(1) {
+ text-align: right;
+ }
+ #options td:nth-child(2),
+ #options td:nth-child(3) {
+ padding-left: 0.5em;
+ }
+ #options td:nth-child(3) {
+ width: 100%; /* to make the last column use the space */
+ }
+ #info {
+ font-family: monospace;
+ }
+ #progress {
+ position: fixed;
+ display: flex;
+ width: 100%;
+ left: 0;
+ top: 0;
+ background-color: #000;
+ color: #fff;
+ align-items: center;
+ }
+ #progress .progress-test-name {
+ flex: 1 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ direction: rtl;
+ white-space: nowrap;
+ }
+ #resultsJSON {
+ font-family: monospace;
+ width: 100%;
+ height: 15em;
+ }
+
+ #resultsVis {
+ border-right: 1px solid var(--results-fg-color);
+ }
+
+ /* tree nodes */
+
+ .nodeheader {
+ display: flex;
+ width: 100%;
+ padding: 0px 2px 0px 1px;
+ }
+ .nodeheader:hover {
+ background: var(--node-hover-bg-color);
+ }
+ .subtreerun,
+ .leafrun,
+ .nodelink,
+ .collapsebtn,
+ .testcaselogbtn {
+ display: inline-flex;
+ flex-shrink: 0;
+ flex-grow: 0;
+ justify-content: center;
+ align-items: center;
+ text-decoration: none;
+ vertical-align: top;
+ color: var(--fg-color);
+ background-color: var(--testcaselogbtn-bg-color);
+ background-repeat: no-repeat;
+ background-position: center;
+ border: 1px solid var(--border-color);
+ }
+ .subtreerun::before,
+ .leafrun::before,
+ .nodelink::before,
+ .collapsebtn::before,
+ .testcaselogbtn::before {
+ content: "";
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ filter: var(--button-image-filter);
+ }
+ @media (pointer: fine) {
+ .subtreerun,
+ .leafrun,
+ .nodelink,
+ .collapsebtn,
+ .testcaselogbtn {
+ flex-basis: 24px;
+ border-radius: 4px;
+ width: 24px;
+ height: 18px;
+ }
+ }
+ @media (pointer: coarse) {
+ .subtreerun,
+ .leafrun,
+ .nodelink,
+ .collapsebtn,
+ .testcaselogbtn {
+ flex-basis: 36px;
+ border-radius: 6px;
+ width: 36px;
+ height: 36px;
+ }
+ }
+ .subtreerun::before {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJAQMAAADaX5RTAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB5JREFUCNdjOMDAsIGBoYeBoZmBoaEBRPaARQ4wAABTfwX/l/WQvgAAAABJRU5ErkJggg==);
+ }
+ .leafrun::before {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkYGD4zwABjFAagwJJwBTBJDEUY1OEoRifIrhiYhSBHYvuJnSHM5LtJry+wxlOAGPTCQmAB/WwAAAAAElFTkSuQmCC);
+ }
+ .nodelink::before {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAACRJREFUCNdjYGBg+P+BoUGAYesFhj4BhvsFDPYNDHwMCMTAAACqJwbp3VgbrAAAAABJRU5ErkJggg==);
+ }
+ .nodetitle {
+ display: inline;
+ flex: 10 0 4em;
+ }
+ .nodecolumns {
+ position: absolute;
+ left: 220px;
+ }
+ .nodequery {
+ font-weight: bold;
+ background: transparent;
+ border: none;
+ padding: 2px;
+ margin: 0 0.5em;
+ width: calc(100vw - 360px);
+ }
+ .nodedescription {
+ margin: 0 0 0 1em;
+ color: var(--node-description-fg-color);
+ white-space: pre-wrap;
+ font-size: 80%;
+ }
+
+ /* tree nodes which are subtrees */
+
+ .subtree {
+ margin: 3px 0 0 0;
+ padding: 3px 0 0 3px;
+ border-width: 1px 0 0;
+ border-style: solid;
+ border-color: var(--subtree-border-color);
+ }
+ .subtree::before {
+ float: right;
+ margin-right: 3px;
+ }
+ .subtree[data-status='fail'], .subtree[data-status='passfail'] {
+ background: linear-gradient(90deg, var(--testcase-data-status-fail-bg-color), var(--testcase-data-status-fail-bg-color) 16px, var(--bg-color) 16px);
+ }
+ .subtree[data-status='fail']::before {
+ content: "⛔"
+ }
+ .subtree[data-status='pass'] {
+ background: linear-gradient(90deg, var(--testcase-data-status-pass-bg-color), var(--testcase-data-status-pass-bg-color) 16px, var(--bg-color) 16px);
+ }
+ .subtree[data-status='pass']::before {
+ content: "✔"
+ }
+ .subtree[data-status='passfail']::before {
+ content: "✔/⛔"
+ }
+ .subtree:hover {
+ border-left-color: var(--subtree-hover-left-border-color);
+ }
+ .subtree.multifile > .subtreechildren > .subtree.multitest,
+ .subtree.multifile > .subtreechildren > .subtree.multicase {
+ border-width: 2px 0 0 1px;
+ border-color: var(--multicase-border-color);
+ }
+ .subtree.multitest > .subtreechildren > .subtree.multicase,
+ .subtree.multitest > .subtreechildren > .testcase {
+ border-width: 2px 0 0 1px;
+ border-color: var(--testcase-border-color);
+ }
+ .subtreechildren {
+ margin-left: 9px;
+ }
+
+ /* tree nodes which are test cases */
+
+ .testcase {
+ padding: 3px;
+ border-width: 1px 0 0 0;
+ border-style: solid;
+ border-color: var(--border-color);
+ background: var(--testcase-bg-color);
+ }
+ .testcase:first-child {
+ margin-top: 3px;
+ }
+ .testcase::after {
+ float: right;
+ margin-top: -1.1em;
+ }
+ .testcase[data-status='fail'] {
+ background: var(--testcase-data-status-fail-bg-color);
+ }
+ .testcase[data-status='fail']::after {
+ content: "⛔"
+ }
+ .testcase[data-status='warn'] {
+ background: var(--testcase-data-status-warn-bg-color);
+ }
+ .testcase[data-status='warn']::after {
+ content: "⚠"
+ }
+ .testcase[data-status='pass'] {
+ background: var(--testcase-data-status-pass-bg-color);
+ }
+ .testcase[data-status='pass']::after {
+ content: "✔"
+ }
+ .testcase[data-status='skip'] {
+ background: var(--testcase-data-status-skip-bg-color);
+ }
+ .testcase .nodequery {
+ font-weight: normal;
+ width: calc(100vw - 275px);
+ }
+ .testcasetime {
+ white-space: nowrap;
+ text-align: right;
+ flex: 1 0 5.5em;
+ }
+ .testcaselogs {
+ margin-left: 6px;
+ width: calc(100% - 6px);
+ border-width: 0 0px 0 1px;
+ border-style: solid;
+ border-color: var(--border-color);
+ background: var(--testcase-logs-bg-color);
+ }
+ .testcaselog {
+ display: flex;
+ }
+ .testcaselog:nth-child(odd) {
+ background: var(--testcase-log-odd-bg-color);
+ }
+ .testcaselog:nth-child(even) {
+ background: var(--testcase-log-even-bg-color);
+ }
+ .testcaselogbtn::before {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAACRJREFUCNdjYGBg+H+AwUGBwV+BQUGAQX0CiNQQYFABk8ogLgBsYQUt2gNKPwAAAABJRU5ErkJggg==);
+ }
+ .testcaselogtext {
+ flex: 1 0;
+ font-size: 10pt;
+ white-space: pre-wrap;
+ word-break: break-word;
+ margin: 0;
+ color: var(--testcase-log-text-fg-color)
+ }
+ .testcaselogtext::first-line {
+ color: var(--testcase-log-text-first-line-fg-color);
+ }
+
+ @media only screen and (max-width: 600px) {
+ .subtreechildren {
+ margin-left: 2px;
+ }
+ .testcaselogs {
+ margin-left: 2px;
+ width: calc(100% - 2px);
+ }
+ .nodequery {
+ position: relative;
+ left: 0;
+ width: 100%;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <h1><img class="logo" src="webgpu-logo-notext.svg">WebGPU Conformance Test Suite</h1>
+ <details>
+ <summary>options (requires reload!)</summary>
+ <table id="options">
+ <tbody></tbody>
+ </table>
+ <p class="important">Note: The options above only set the url parameters.
+ You must reload the page for the options to take affect.</p>
+ </details>
+ <p>
+ <input type=button id=expandall value="Expand All (slow!)">
+ <label><input type=checkbox id=autoCloseOnPass> Auto-close each subtree when it passes</label>
+ </p>
+
+ <div id="info"></div>
+ <div id="resultsVis"></div>
+ <div id="progress" style="display: none;"><button type="button">stop</button><div class="progress-test-name"></div></div>
+
+ <p>
+ <input type="button" id="copyResultsJSON" value="Copy results as JSON">
+ </p>
+
+ <script type="module" src="../out/common/runtime/standalone.js"></script>
+ </body>
+</html>
diff --git a/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/LICENSE.txt b/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/LICENSE.txt
new file mode 100644
index 0000000000..45ee6cbe38
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/LICENSE.txt
@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/jquery-3.3.1.min.js b/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/jquery-3.3.1.min.js
new file mode 100644
index 0000000000..4d9b3a2587
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/standalone/third_party/jquery/jquery-3.3.1.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:"jQuery"+("3.3.1"+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==c.call(e))&&(!(t=i(e))||"function"==typeof(n=f.call(t,"constructor")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),"function"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function C(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",I="\\["+M+"*("+R+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+R+"))|)"+M+"*\\]",W=":("+R+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+I+")*)|.*)\\)|)",$=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),F=new RegExp("^"+M+"*,"+M+"*"),_=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=fe(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=pe(t);function ye(){}ye.prototype=r.filters=r.pseudos,r.setFilters=new ye,a=oe.tokenize=function(e,t){var n,i,o,a,s,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=r.preFilter;while(s){n&&!(i=F.exec(s))||(i&&(s=s.slice(i[0].length)||s),u.push(o=[])),n=!1,(i=_.exec(s))&&(n=i.shift(),o.push({value:n,type:i[0].replace(B," ")}),s=s.slice(n.length));for(a in r.filter)!(i=V[a].exec(s))||l[a]&&!(i=l[a](i))||(n=i.shift(),o.push({value:n,type:a,matches:i}),s=s.slice(n.length));if(!n)break}return t?s.length:s?oe.error(e):k(e,u).slice(0)};function ve(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function me(e,t,n){var r=t.dir,i=t.next,o=i||r,a=n&&"parentNode"===o,s=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||a)return e(t,n,i);return!1}:function(t,n,u){var l,c,f,p=[T,s];if(u){while(t=t[r])if((1===t.nodeType||a)&&e(t,n,u))return!0}else while(t=t[r])if(1===t.nodeType||a)if(f=t[b]||(t[b]={}),c=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((l=c[o])&&l[0]===T&&l[1]===s)return p[2]=l[2];if(c[o]=p,p[2]=e(t,n,u))return!0}return!1}}function xe(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r<i;r++)oe(e,t[r],n);return n}function we(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Te(e,t,n,r,i,o){return r&&!r[b]&&(r=Te(r)),i&&!i[b]&&(i=Te(i,o)),se(function(o,a,s,u){var l,c,f,p=[],d=[],h=a.length,g=o||be(t||"*",s.nodeType?[s]:s,[]),y=!e||!o&&t?g:we(g,p,e,s,u),v=n?i||(o?e:h||r)?[]:a:y;if(n&&n(y,v,s,u),r){l=we(v,d),r(l,[],s,u),c=l.length;while(c--)(f=l[c])&&(v[d[c]]=!(y[d[c]]=f))}if(o){if(i||e){if(i){l=[],c=v.length;while(c--)(f=v[c])&&l.push(y[c]=f);i(null,v=[],l,u)}c=v.length;while(c--)(f=v[c])&&(l=i?O(o,f):p[c])>-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u<o;u++)if(n=r.relative[e[u].type])p=[me(xe(p),n)];else{if((n=r.filter[e[u].type].apply(null,e[u].matches))[b]){for(i=++u;i<o;i++)if(r.relative[e[i].type])break;return Te(u>1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u<i&&Ce(e.slice(u,i)),i<o&&Ce(e=e.slice(i)),i<o&&ve(e))}p.push(n)}return xe(p)}function Ee(e,t){var n=t.length>0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t<r;t++)if(w.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)w.find(e,i[t],n);return r>1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(w.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&w(e);if(!D.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s<o.length)!1===o[s].apply(n[0],n[1])&&e.stopOnFalse&&(s=o.length,n=!1)}e.memory||(n=!1),t=!1,i&&(o=n?[]:"")},l={add:function(){return o&&(n&&!t&&(s=o.length-1,a.push(n)),function t(n){w.each(n,function(n,r){g(r)?e.unique&&l.has(r)||o.push(r):r&&r.length&&"string"!==x(r)&&t(r)})}(arguments),n&&!t&&u()),this},remove:function(){return w.each(arguments,function(e,t){var n;while((n=w.inArray(t,o,n))>-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t<o)){if((e=r.apply(s,u))===n.promise())throw new TypeError("Thenable self-resolution");l=e&&("object"==typeof e||"function"==typeof e)&&e.then,g(l)?i?l.call(e,a(o,n,I,i),a(o,n,W,i)):(o++,l.call(e,a(o,n,I,i),a(o,n,W,i),a(o,n,I,n.notifyWith))):(r!==I&&(s=void 0,u=[e]),(i||n.resolveWith)(s,u))}},c=i?l:function(){try{l()}catch(e){w.Deferred.exceptionHook&&w.Deferred.exceptionHook(e,c.stackTrace),t+1>=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},X=/^-ms-/,U=/-([a-z])/g;function V(e,t){return t.toUpperCase()}function G(e){return e.replace(X,"ms-").replace(U,V)}var Y=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Q(){this.expando=w.expando+Q.uid++}Q.uid=1,Q.prototype={cache:function(e){var t=e[this.expando];return t||(t={},Y(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[G(t)]=n;else for(r in t)i[G(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][G(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(G):(t=G(t))in r?[t]:t.match(M)||[]).length;while(n--)delete r[t[n]]}(void 0===t||w.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!w.isEmptyObject(t)}};var J=new Q,K=new Q,Z=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,ee=/[A-Z]/g;function te(e){return"true"===e||"false"!==e&&("null"===e?null:e===+e+""?+e:Z.test(e)?JSON.parse(e):e)}function ne(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(ee,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n=te(n)}catch(e){}K.set(e,t,n)}else n=void 0;return n}w.extend({hasData:function(e){return K.hasData(e)||J.hasData(e)},data:function(e,t,n){return K.access(e,t,n)},removeData:function(e,t){K.remove(e,t)},_data:function(e,t,n){return J.access(e,t,n)},_removeData:function(e,t){J.remove(e,t)}}),w.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=K.get(o),1===o.nodeType&&!J.get(o,"hasDataAttrs"))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf("data-")&&(r=G(r.slice(5)),ne(o,r,i[r]));J.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof e?this.each(function(){K.set(this,e)}):z(this,function(t){var n;if(o&&void 0===t){if(void 0!==(n=K.get(o,e)))return n;if(void 0!==(n=ne(o,e)))return n}else this.each(function(){K.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?w.queue(this[0],e):void 0===t?this:this.each(function(){var n=w.queue(this,e,t);w._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&w.dequeue(this,e)})},dequeue:function(e){return this.each(function(){w.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=w.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=J.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var re=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ie=new RegExp("^(?:([+-])=|)("+re+")([a-z%]*)$","i"),oe=["Top","Right","Bottom","Left"],ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&w.contains(e.ownerDocument,e)&&"none"===w.css(e,"display")},se=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};function ue(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return w.css(e,t,"")},u=s(),l=n&&n[3]||(w.cssNumber[t]?"":"px"),c=(w.cssNumber[t]||"px"!==l&&+u)&&ie.exec(w.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)w.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,w.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var le={};function ce(e){var t,n=e.ownerDocument,r=e.nodeName,i=le[r];return i||(t=n.body.appendChild(n.createElement(r)),i=w.css(t,"display"),t.parentNode.removeChild(t),"none"===i&&(i="block"),le[r]=i,i)}function fe(e,t){for(var n,r,i=[],o=0,a=e.length;o<a;o++)(r=e[o]).style&&(n=r.style.display,t?("none"===n&&(i[o]=J.get(r,"display")||null,i[o]||(r.style.display="")),""===r.style.display&&ae(r)&&(i[o]=ce(r))):"none"!==n&&(i[o]="none",J.set(r,"display",n)));for(o=0;o<a;o++)null!=i[o]&&(e[o].style.display=i[o]);return e}w.fn.extend({show:function(){return fe(this,!0)},hide:function(){return fe(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?w(this).show():w(this).hide()})}});var pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n<r;n++)J.set(e[n],"globalEval",!t||J.get(t[n],"globalEval"))}var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===x(o))w.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+w.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;w.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&w.inArray(o,r)>-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="<textarea>x</textarea>",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n<arguments.length;n++)u[n]=arguments[n];if(t.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,t)){s=w.event.handlers.call(this,t,l),n=0;while((o=s[n++])&&!t.isPropagationStopped()){t.currentTarget=o.elem,r=0;while((a=o.handlers[r++])&&!t.isImmediatePropagationStopped())t.rnamespace&&!t.rnamespace.test(a.namespace)||(t.handleObj=a,t.data=a.data,void 0!==(i=((w.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(t.result=i)&&(t.preventDefault(),t.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,t),t.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&e.button>=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?w(i,this).index(l)>-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(e,t){Object.defineProperty(w.Event.prototype,e,{enumerable:!0,configurable:!0,get:g(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(e){return e[w.expando]?e:new w.Event(e)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==Se()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===Se()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&N(this,"input"))return this.click(),!1},_default:function(e){return N(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},w.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},w.Event=function(e,t){if(!(this instanceof w.Event))return new w.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ee:ke,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&w.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[w.expando]=!0},w.Event.prototype={constructor:w.Event,isDefaultPrevented:ke,isPropagationStopped:ke,isImmediatePropagationStopped:ke,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ee,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ee,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ee,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},w.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&we.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&Te.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},w.event.addProp),w.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){w.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return i&&(i===r||w.contains(r,i))||(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),w.fn.extend({on:function(e,t,n,r){return De(this,e,t,n,r)},one:function(e,t,n,r){return De(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,w(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=ke),this.each(function(){w.event.remove(this,e,n,t)})}});var Ne=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/<script|<style|<link/i,je=/checked\s*(?:[^=]|=\s*.checked.)/i,qe=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n<r;n++)w.event.add(t,i,l[i][n])}K.hasData(e)&&(s=K.access(e),u=w.extend({},s),K.set(t,u))}}function Me(e,t){var n=t.nodeName.toLowerCase();"input"===n&&pe.test(e.type)?t.checked=e.checked:"input"!==n&&"textarea"!==n||(t.defaultValue=e.defaultValue)}function Re(e,t,n,r){t=a.apply([],t);var i,o,s,u,l,c,f=0,p=e.length,d=p-1,y=t[0],v=g(y);if(v||p>1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f<p;f++)l=i,f!==d&&(l=w.clone(l,!0,!0),u&&w.merge(s,ye(l,"script"))),n.call(e[f],l,f);if(u)for(c=s[s.length-1].ownerDocument,w.map(s,Oe),f=0;f<u;f++)l=s[f],he.test(l.type||"")&&!J.access(l,"globalEval")&&w.contains(c,l)&&(l.src&&"module"!==(l.type||"").toLowerCase()?w._evalUrl&&w._evalUrl(l.src):m(l.textContent.replace(qe,""),c,l))}return e}function Ie(e,t,n){for(var r,i=t?w.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||w.cleanData(ye(r)),r.parentNode&&(n&&w.contains(r.ownerDocument,r)&&ve(ye(r,"script")),r.parentNode.removeChild(r));return e}w.extend({htmlPrefilter:function(e){return e.replace(Ne,"<$1></$2>")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r<i;r++)Me(o[r],a[r]);if(t)if(n)for(o=o||ye(e),a=a||ye(s),r=0,i=o.length;r<i;r++)Pe(o[r],a[r]);else Pe(e,s);return(a=ye(s,"script")).length>0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(w.cleanData(ye(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=[];return Re(this,arguments,function(t){var n=this.parentNode;w.inArray(this,e)<0&&(w.cleanData(ye(this)),n&&n.replaceChild(t,this))},e)}}),w.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){w.fn[e]=function(e){for(var n,r=[],i=w(e),o=i.length-1,a=0;a<=o;a++)n=a===o?this:this.clone(!0),w(i[a])[t](n),s.apply(r,n.get());return this.pushStack(r)}});var We=new RegExp("^("+re+")(?!px)[a-z%]+$","i"),$e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},Be=new RegExp(oe.join("|"),"i");!function(){function t(){if(c){l.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",c.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",be.appendChild(l).appendChild(c);var t=e.getComputedStyle(c);i="1%"!==t.top,u=12===n(t.marginLeft),c.style.right="60%",s=36===n(t.right),o=36===n(t.width),c.style.position="absolute",a=36===c.offsetWidth||"absolute",be.removeChild(l),c=null}}function n(e){return Math.round(parseFloat(e))}var i,o,a,s,u,l=r.createElement("div"),c=r.createElement("div");c.style&&(c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",h.clearCloneStyle="content-box"===c.style.backgroundClip,w.extend(h,{boxSizingReliable:function(){return t(),o},pixelBoxStyles:function(){return t(),s},pixelPosition:function(){return t(),i},reliableMarginLeft:function(){return t(),u},scrollboxSize:function(){return t(),a}}))}();function Fe(e,t,n){var r,i,o,a,s=e.style;return(n=n||$e(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||w.contains(e.ownerDocument,e)||(a=w.style(e,t)),!h.pixelBoxStyles()&&We.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function _e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}var ze=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ue={position:"absolute",visibility:"hidden",display:"block"},Ve={letterSpacing:"0",fontWeight:"400"},Ge=["Webkit","Moz","ms"],Ye=r.createElement("div").style;function Qe(e){if(e in Ye)return e;var t=e[0].toUpperCase()+e.slice(1),n=Ge.length;while(n--)if((e=Ge[n]+t)in Ye)return e}function Je(e){var t=w.cssProps[e];return t||(t=w.cssProps[e]=Qe(e)||e),t}function Ke(e,t,n){var r=ie.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ze(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=w.css(e,n+oe[a],!0,i)),r?("content"===n&&(u-=w.css(e,"padding"+oe[a],!0,i)),"margin"!==n&&(u-=w.css(e,"border"+oe[a]+"Width",!0,i))):(u+=w.css(e,"padding"+oe[a],!0,i),"padding"!==n?u+=w.css(e,"border"+oe[a]+"Width",!0,i):s+=w.css(e,"border"+oe[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a<i;a++)o[t[a]]=w.css(e,t[a],!1,r);return o}return void 0!==n?w.style(e,t,n):w.css(e,t)},e,t,arguments.length>1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function ct(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),y=J.get(e,"fxshow");n.queue||(null==(a=w._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,w.queue(e,"fx").length||a.empty.fire()})}));for(r in t)if(i=t[r],it.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!y||void 0===y[r])continue;g=!0}d[r]=y&&y[r]||w.style(e,r)}if((u=!w.isEmptyObject(t))||!w.isEmptyObject(d)){f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=y&&y.display)&&(l=J.get(e,"display")),"none"===(c=w.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=w.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===w.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1;for(r in d)u||(y?"hidden"in y&&(g=y.hidden):y=J.access(e,"fxshow",{display:l}),o&&(y.hidden=!g),g&&fe([e],!0),p.done(function(){g||fe([e]),J.remove(e,"fxshow");for(r in d)w.style(e,r,d[r])})),u=lt(g?y[r]:0,r,p),r in y||(y[r]=u.start,g&&(u.end=u.start,u.start=0))}}function ft(e,t){var n,r,i,o,a;for(n in e)if(r=G(n),i=t[r],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=w.cssHooks[r])&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function pt(e,t,n){var r,i,o=0,a=pt.prefilters.length,s=w.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;for(var t=nt||st(),n=Math.max(0,l.startTime+l.duration-t),r=1-(n/l.duration||0),o=0,a=l.tweens.length;o<a;o++)l.tweens[o].run(r);return s.notifyWith(e,[l,r,n]),r<1&&a?n:(a||s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l]),!1)},l=s.promise({elem:e,props:w.extend({},t),opts:w.extend(!0,{specialEasing:{},easing:w.easing._default},n),originalProperties:t,originalOptions:n,startTime:nt||st(),duration:n.duration,tweens:[],createTween:function(t,n){var r=w.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;n<r;n++)l.tweens[n].run(1);return t?(s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l,t])):s.rejectWith(e,[l,t]),this}}),c=l.props;for(ft(c,l.opts.specialEasing);o<a;o++)if(r=pt.prefilters[o].call(l,e,c,l.opts))return g(r.stop)&&(w._queueHooks(l.elem,l.opts.queue).stop=r.stop.bind(r)),r;return w.map(c,lt,l),g(l.opts.start)&&l.opts.start.call(e,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),w.fx.timer(w.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l}w.Animation=w.extend(pt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return ue(n.elem,e,ie.exec(t),n),n}]},tweener:function(e,t){g(e)?(t=e,e=["*"]):e=e.match(M);for(var n,r=0,i=e.length;r<i;r++)n=e[r],pt.tweeners[n]=pt.tweeners[n]||[],pt.tweeners[n].unshift(t)},prefilters:[ct],prefilter:function(e,t){t?pt.prefilters.unshift(e):pt.prefilters.push(e)}}),w.speed=function(e,t,n){var r=e&&"object"==typeof e?w.extend({},e):{complete:n||!n&&t||g(e)&&e,duration:e,easing:n&&t||t&&!g(t)&&t};return w.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in w.fx.speeds?r.duration=w.fx.speeds[r.duration]:r.duration=w.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){g(r.old)&&r.old.call(this),r.queue&&w.dequeue(this,r.queue)},r},w.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=w.isEmptyObject(e),o=w.speed(t,n,r),a=function(){var t=pt(this,w.extend({},e),o);(i||J.get(this,"finish"))&&t.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&!1!==e&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=w.timers,a=J.get(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&ot.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));!t&&n||w.dequeue(this,e)})},finish:function(e){return!1!==e&&(e=e||"fx"),this.each(function(){var t,n=J.get(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=w.timers,a=r?r.length:0;for(n.finish=!0,w.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;t<a;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),w.each(["toggle","show","hide"],function(e,t){var n=w.fn[t];w.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ut(t,!0),e,r,i)}}),w.each({slideDown:ut("show"),slideUp:ut("hide"),slideToggle:ut("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){w.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),w.timers=[],w.fx.tick=function(){var e,t=0,n=w.timers;for(nt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||w.fx.stop(),nt=void 0},w.fx.timer=function(e){w.timers.push(e),w.fx.start()},w.fx.interval=13,w.fx.start=function(){rt||(rt=!0,at())},w.fx.stop=function(){rt=null},w.fx.speeds={slow:600,fast:200,_default:400},w.fn.delay=function(t,n){return t=w.fx?w.fx.speeds[t]||t:t,n=n||"fx",this.queue(n,function(n,r){var i=e.setTimeout(n,t);r.stop=function(){e.clearTimeout(i)}})},function(){var e=r.createElement("input"),t=r.createElement("select").appendChild(r.createElement("option"));e.type="checkbox",h.checkOn=""!==e.value,h.optSelected=t.selected,(e=r.createElement("input")).value="t",e.type="radio",h.radioValue="t"===e.value}();var dt,ht=w.expr.attrHandle;w.fn.extend({attr:function(e,t){return z(this,w.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!N(n.parentNode,"optgroup"))){if(t=w(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=w.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=w.inArray(w.valHooks.option.get(r),o)>-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("<script>").prop({charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&o("error"===e.type?404:200,e.type)}),r.head.appendChild(t[0])},abort:function(){n&&n()}}}});var Yt=[],Qt=/(=)\?(?=&|$)|\?\?/;w.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Yt.pop()||w.expando+"_"+Et++;return this[e]=!0,e}}),w.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(Qt.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Qt.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=g(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Qt,"$1"+i):!1!==t.jsonp&&(t.url+=(kt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||w.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?w(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,Yt.push(i)),a&&g(o)&&o(a[0]),a=o=void 0}),"script"}),h.createHTMLDocument=function(){var e=r.implementation.createHTMLDocument("").body;return e.innerHTML="<form></form><form></form>",2===e.childNodes.length}(),w.parseHTML=function(e,t,n){if("string"!=typeof e)return[];"boolean"==typeof t&&(n=t,t=!1);var i,o,a;return t||(h.createHTMLDocument?((i=(t=r.implementation.createHTMLDocument("")).createElement("base")).href=r.location.href,t.head.appendChild(i)):t=r),o=A.exec(e),a=!n&&[],o?[t.createElement(o[1])]:(o=xe([e],t,a),a&&a.length&&w(a).remove(),w.merge([],o.childNodes))},w.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=vt(e.slice(s)),e=e.slice(0,s)),g(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&w.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?w("<div>").append(w.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},w.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){w.fn[t]=function(e){return this.on(t,e)}}),w.expr.pseudos.animated=function(e){return w.grep(w.timers,function(t){return e===t.elem}).length},w.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l,c=w.css(e,"position"),f=w(e),p={};"static"===c&&(e.style.position="relative"),s=f.offset(),o=w.css(e,"top"),u=w.css(e,"left"),(l=("absolute"===c||"fixed"===c)&&(o+u).indexOf("auto")>-1)?(a=(r=f.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),g(t)&&(t=t.call(e,n,w.extend({},s))),null!=t.top&&(p.top=t.top-s.top+a),null!=t.left&&(p.left=t.left-s.left+i),"using"in t?t.using.call(e,p):f.css(p)}},w.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){w.offset.setOffset(this,e,t)});var t,n,r=this[0];if(r)return r.getClientRects().length?(t=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===w.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===w.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=w(e).offset()).top+=w.css(e,"borderTopWidth",!0),i.left+=w.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-w.css(r,"marginTop",!0),left:t.left-i.left-w.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===w.css(e,"position"))e=e.offsetParent;return e||be})}}),w.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n="pageYOffset"===t;w.fn[e]=function(r){return z(this,function(e,r,i){var o;if(y(e)?o=e:9===e.nodeType&&(o=e.defaultView),void 0===i)return o?o[t]:e[r];o?o.scrollTo(n?o.pageXOffset:i,n?i:o.pageYOffset):e[r]=i},e,r,arguments.length)}}),w.each(["top","left"],function(e,t){w.cssHooks[t]=_e(h.pixelPosition,function(e,n){if(n)return n=Fe(e,t),We.test(n)?w(e).position()[t]+"px":n})}),w.each({Height:"height",Width:"width"},function(e,t){w.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){w.fn[r]=function(i,o){var a=arguments.length&&(n||"boolean"!=typeof i),s=n||(!0===i||!0===o?"margin":"border");return z(this,function(t,n,i){var o;return y(t)?0===r.indexOf("outer")?t["inner"+e]:t.document.documentElement["client"+e]:9===t.nodeType?(o=t.documentElement,Math.max(t.body["scroll"+e],o["scroll"+e],t.body["offset"+e],o["offset"+e],o["client"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),w.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},"function"==typeof define&&define.amd&&define("jquery",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});
diff --git a/dom/webgpu/tests/cts/checkout/standalone/third_party/normalize.min.css b/dom/webgpu/tests/cts/checkout/standalone/third_party/normalize.min.css
new file mode 100644
index 0000000000..8ba678f608
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/standalone/third_party/normalize.min.css
@@ -0,0 +1 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
diff --git a/dom/webgpu/tests/cts/checkout/standalone/webgpu-logo-notext.svg b/dom/webgpu/tests/cts/checkout/standalone/webgpu-logo-notext.svg
new file mode 100644
index 0000000000..8e8c2bf72c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/standalone/webgpu-logo-notext.svg
@@ -0,0 +1,34 @@
+<svg id="Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="768" height="600" viewBox="0 0 768 600">
+ <defs>
+ <style>
+ .cls-1 {
+ fill: #005a9c;
+ }
+
+ .cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6 {
+ fill-rule: evenodd;
+ }
+
+ .cls-2 {
+ fill: #0066b0;
+ }
+
+ .cls-3 {
+ fill: #0076cc;
+ }
+
+ .cls-4 {
+ fill: #0086e8;
+ }
+
+ .cls-5 {
+ fill: #0093ff;
+ }
+ </style>
+ </defs>
+ <path id="Triangle_1" data-name="Triangle 1" class="cls-1" d="M265.5,504L24.745,87h481.51Z"/>
+ <path id="Triangle_2" data-name="Triangle 2" class="cls-2" d="M506.5,87L386,295H627Z"/>
+ <path id="Triangle_3" data-name="Triangle 3" class="cls-3" d="M506.5,503L386,295H627Z"/>
+ <path id="Triangle_4" data-name="Triangle 4" class="cls-4" d="M626.5,296L566,192H687Z"/>
+ <path id="Triangle_5" data-name="Triangle 5" class="cls-5" d="M626.5,88L566,192H687Z"/>
+</svg>
diff --git a/dom/webgpu/tests/cts/checkout/tools/checklist b/dom/webgpu/tests/cts/checkout/tools/checklist
new file mode 100644
index 0000000000..8aace4f387
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/checklist
@@ -0,0 +1,11 @@
+#!/usr/bin/env node
+
+// Takes a list of queries and checks that:
+// - Every query matches something in the repository
+// - Every case in the repository matches exactly one query
+// This is used to ensure that tracking spreadsheet is complete (not missing any tests)
+// and every query in it is valid (e.g. renames have been applied, and new tests added
+// to the spreadsheet have also been added to the CTS).
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/checklist.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/dev_server b/dom/webgpu/tests/cts/checkout/tools/dev_server
new file mode 100644
index 0000000000..d400d79c19
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/dev_server
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/dev_server.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/gen_cache b/dom/webgpu/tests/cts/checkout/tools/gen_cache
new file mode 100644
index 0000000000..fd7bf52c2f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/gen_cache
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/gen_cache.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/gen_listings b/dom/webgpu/tests/cts/checkout/tools/gen_listings
new file mode 100644
index 0000000000..6c25622423
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/gen_listings
@@ -0,0 +1,7 @@
+#!/usr/bin/env node
+
+// Crawl a suite directory (e.g. src/webgpu/) to generate a listing.js containing
+// the listing of test files in the suite.
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/gen_listings.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/gen_version b/dom/webgpu/tests/cts/checkout/tools/gen_version
new file mode 100644
index 0000000000..b35f236f31
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/gen_version
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+
+// Get the current git hash, and save (overwrite) it into out/framework/version.js
+// so it can be read when running inside the browser.
+
+/* eslint-disable no-console */
+
+require('../src/common/tools/setup-ts-in-node.js');
+const fs = require('fs');
+
+const myself = 'tools/gen_version';
+if (!fs.existsSync(myself)) {
+ console.error('Must be run from repository root');
+ process.exit(1);
+}
+
+const { version } = require('../src/common/tools/version.ts');
+
+fs.mkdirSync('./out/common/framework', { recursive: true });
+// Overwrite the version.js generated by TypeScript compilation.
+fs.writeFileSync(
+ './out/common/internal/version.js',
+ `\
+// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
+
+export const version = '${version}';
+`
+);
+
+// Since the generated version.js was overwritten, its source map is no longer relevant.
+try {
+ fs.unlinkSync('./out/common/internal/version.js.map');
+} catch (ex) { }
diff --git a/dom/webgpu/tests/cts/checkout/tools/gen_wpt_cts_html b/dom/webgpu/tests/cts/checkout/tools/gen_wpt_cts_html
new file mode 100644
index 0000000000..07f1f465c7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/gen_wpt_cts_html
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+
+// Generate a top-level cts.https.html file for WPT.
+//
+// In the default invocation (used by grunt), just generates a cts.https.html with one "variant"
+// per test spec file (.spec.ts).
+//
+// In the advanced invocation, generate a list of variants, which are broken down as much as needed
+// to accommodate a provided list of suppressions, and no further. This reduces the total runtime of
+// the test suite by not generating an entire page load for every single test case.
+// The resulting cts.https.html can be checked in and used to run tests within browser harnesses.
+//
+// For example, for the following 9 cases:
+//
+// webgpu:a/foo:foo1={"x":1}
+// webgpu:a/foo:foo1={"x":2}
+// webgpu:a/foo:foo2={"x":1}
+// webgpu:a/foo:foo2={"x":2}
+// webgpu:a/bar:bar1={"x":1}
+// webgpu:a/bar:bar1={"x":2}
+// webgpu:a/bar:bar1={"x":3}
+// webgpu:a/bar:bar2={"x":1}
+// webgpu:a/bar:bar2={"x":2}
+//
+// and the following suppressions:
+//
+// [ Win ] ?q=webgpu:a/bar:bar1={"x":1} [ Failure ]
+// [ Mac ] ?q=webgpu:a/bar:bar1={"x":3} [ Failure ]
+//
+// the following list of 5 variants gives enough granularity to suppress only the failing cases:
+//
+// ?q=webgpu:a/foo:
+// ?q=webgpu:a/bar:bar1={"x":1} <- [ Win ]
+// ?q=webgpu:a/bar:bar1={"x":2}
+// ?q=webgpu:a/bar:bar1={"x":3} <- [ Mac ]
+// ?q=webgpu:a/bar:bar2~
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/gen_wpt_cts_html.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/presubmit b/dom/webgpu/tests/cts/checkout/tools/presubmit
new file mode 100644
index 0000000000..a2a3b78690
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/presubmit
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/presubmit.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/run_deno b/dom/webgpu/tests/cts/checkout/tools/run_deno
new file mode 100644
index 0000000000..8cc89a475c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/run_deno
@@ -0,0 +1,3 @@
+#!/usr/bin/env -S deno run --unstable --allow-read --allow-write --allow-env --allow-net=deno.land --no-check
+
+import '../out/common/runtime/cmdline.js'; \ No newline at end of file
diff --git a/dom/webgpu/tests/cts/checkout/tools/run_node b/dom/webgpu/tests/cts/checkout/tools/run_node
new file mode 100644
index 0000000000..b71ec9f134
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/run_node
@@ -0,0 +1,6 @@
+#!/usr/bin/env node
+
+// Run test suites under node.
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/runtime/cmdline.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tools/run_wpt_ref_tests b/dom/webgpu/tests/cts/checkout/tools/run_wpt_ref_tests
new file mode 100644
index 0000000000..79fd1b1b7c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tools/run_wpt_ref_tests
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+
+require('../src/common/tools/setup-ts-in-node.js');
+require('../src/common/tools/run_wpt_ref_tests.ts');
diff --git a/dom/webgpu/tests/cts/checkout/tsconfig.json b/dom/webgpu/tests/cts/checkout/tsconfig.json
new file mode 100644
index 0000000000..2b60900598
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/tsconfig.json
@@ -0,0 +1,49 @@
+{
+ "extends": "./node_modules/gts/tsconfig-google.json",
+ "compilerOptions": {
+ "lib": ["dom", "es2019"],
+ "module": "esnext",
+ /* Output options */
+ "noEmit": true,
+ /* Strict type-checking options */
+ "allowJs": true,
+ "strict": true,
+ /* tsc lint options */
+ "noImplicitReturns": true,
+ /* These should be caught by eslint instead */
+ "noFallthroughCasesInSwitch": false,
+ "noUnusedLocals": false,
+ "allowUnreachableCode": true,
+ /* Module Options */
+ "moduleResolution": "node",
+ "esModuleInterop": false,
+ "skipLibCheck": true,
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/external/**/*.js",
+ ],
+ "typedocOptions": {
+ "entryPointStrategy": "expand",
+ "entryPoints": [
+ "src/common/framework/",
+ "src/common/util/",
+ "src/webgpu/",
+ ],
+ "exclude": [
+ "**/*.spec.ts",
+ "**/*.html.ts",
+ "src/*/listing.ts",
+ "src/webgpu/util/device_pool.ts",
+ ],
+ "excludeInternal": true,
+ "excludeProtected": true,
+ "excludePrivate": true,
+ "validation": {
+ "invalidLink": true,
+ "notExported": false,
+ },
+ "readme": "./docs/helper_index.txt",
+ "out": "docs/tsdoc/"
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/w3c.json b/dom/webgpu/tests/cts/checkout/w3c.json
new file mode 100644
index 0000000000..2c0b3a7a90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/w3c.json
@@ -0,0 +1,5 @@
+{
+ "group": [96877, 125519],
+ "contacts": ["tidoust", "Kangz", "grorg"],
+ "repo-type": ["tests"]
+} \ No newline at end of file
diff --git a/dom/webgpu/tests/cts/checkout_commit.txt b/dom/webgpu/tests/cts/checkout_commit.txt
new file mode 100644
index 0000000000..3396f1f6a2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout_commit.txt
@@ -0,0 +1 @@
+b3ce8f38983dc445e66c35d83f1110ce89fba9ba
diff --git a/dom/webgpu/tests/cts/myexpectations.txt b/dom/webgpu/tests/cts/myexpectations.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/webgpu/tests/cts/myexpectations.txt
diff --git a/dom/webgpu/tests/cts/vendor/Cargo.lock b/dom/webgpu/tests/cts/vendor/Cargo.lock
new file mode 100644
index 0000000000..ea51837ca5
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/Cargo.lock
@@ -0,0 +1,889 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3"
+dependencies = [
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "dircpy"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10b6622b9d0dc20c70e74ff24c56493278d7d9299ac8729deb923703616e5a7e"
+dependencies = [
+ "jwalk",
+ "log",
+ "walkdir",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "format"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901e1b63ac63f86d9fb836b1ae8b43e5a9f2338975e9de24f36a1af4acf23ac8"
+dependencies = [
+ "format-core",
+ "format-macro",
+]
+
+[[package]]
+name = "format-core"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e66b70d6700c47044b73e43dd0649e0d6bfef18f87919c23785cdbd1aaa9d3f5"
+
+[[package]]
+name = "format-macro"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f9faac4e57f217563dd1fd58628a0c526aa37a681ffac76ca80d64907370a4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "is_ci"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
+
+[[package]]
+name = "jwalk"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dbcda57db8b6dc067e589628b7348639014e793d9e8137d8cf215e8b133a0bd"
+dependencies = [
+ "crossbeam",
+ "rayon",
+]
+
+[[package]]
+name = "lets_find_up"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91a14fb0b4300e025486cc8bc096c7173c2c615ce8f9c6da7829a4af3f5afbd"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miette"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703"
+dependencies = [
+ "atty",
+ "backtrace",
+ "miette-derive",
+ "once_cell",
+ "owo-colors",
+ "supports-color",
+ "supports-hyperlinks",
+ "supports-unicode",
+ "terminal_size",
+ "textwrap",
+ "thiserror",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "5.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.30.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "rustix"
+version = "0.36.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "smawk"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "supports-color"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f"
+dependencies = [
+ "atty",
+ "is_ci",
+]
+
+[[package]]
+name = "supports-hyperlinks"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406"
+dependencies = [
+ "atty",
+]
+
+[[package]]
+name = "supports-unicode"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2"
+dependencies = [
+ "atty",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
+dependencies = [
+ "hashbrown",
+ "regex",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "vendor-webgpu-cts"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "dircpy",
+ "dunce",
+ "env_logger",
+ "format",
+ "lets_find_up",
+ "log",
+ "miette",
+ "regex",
+ "shell-words",
+ "thiserror",
+ "which",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
diff --git a/dom/webgpu/tests/cts/vendor/Cargo.toml b/dom/webgpu/tests/cts/vendor/Cargo.toml
new file mode 100644
index 0000000000..d7721d1931
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "vendor-webgpu-cts"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.1.6", features = ["derive"] }
+dircpy = "0.3.14"
+dunce = "1.0.3"
+env_logger = "0.10.0"
+format = "0.2.4"
+lets_find_up = "0.0.3"
+log = "0.4.17"
+miette = { version = "5.5.0", features = ["fancy"] }
+regex = "1.7.1"
+shell-words = "1.1.0"
+thiserror = "1.0.38"
+which = "4.4.0"
+
+[workspace]
diff --git a/dom/webgpu/tests/cts/vendor/src/fs.rs b/dom/webgpu/tests/cts/vendor/src/fs.rs
new file mode 100644
index 0000000000..4d27ad00b3
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/src/fs.rs
@@ -0,0 +1,310 @@
+use std::{
+ ffi::OsStr,
+ fmt::{self, Display},
+ fs,
+ ops::Deref,
+ path::{Path, PathBuf, StripPrefixError},
+};
+
+use miette::{ensure, Context, IntoDiagnostic};
+
+#[derive(Debug)]
+pub(crate) struct FileRoot {
+ nickname: &'static str,
+ path: PathBuf,
+}
+
+impl FileRoot {
+ pub(crate) fn new<P>(nickname: &'static str, path: P) -> miette::Result<Self>
+ where
+ P: AsRef<Path>,
+ {
+ let path = path.as_ref();
+ Ok(Self {
+ nickname,
+ path: dunce::canonicalize(path)
+ .map_err(miette::Report::msg)
+ .wrap_err_with(|| format!("failed to canonicalize {path:?}"))?,
+ })
+ }
+
+ pub(crate) fn nickname(&self) -> &str {
+ self.nickname
+ }
+
+ pub(crate) fn try_child<P>(&self, path: P) -> Result<Child<'_>, StripPrefixError>
+ where
+ P: AsRef<Path>,
+ {
+ let path = path.as_ref();
+ if path.is_absolute() {
+ path.strip_prefix(&self.path)?;
+ }
+ Ok(Child {
+ root: self,
+ path: self.path.join(path),
+ })
+ }
+
+ #[track_caller]
+ pub(crate) fn child<P>(&self, path: P) -> Child<'_>
+ where
+ P: AsRef<Path>,
+ {
+ self.try_child(path)
+ .into_diagnostic()
+ .wrap_err("invariant violation: `path` is absolute and not a child of this file root")
+ .unwrap()
+ }
+
+ fn removed_dir<P>(&self, path: P) -> miette::Result<Child<'_>>
+ where
+ P: AsRef<Path>,
+ {
+ let path = path.as_ref();
+ let child = self.child(path);
+ if child.exists() {
+ log::info!("removing old contents of {child}…",);
+ log::trace!("removing directory {:?}", &*child);
+ fs::remove_dir_all(&*child)
+ .map_err(miette::Report::msg)
+ .wrap_err_with(|| format!("failed to remove old contents of {child}"))?;
+ }
+ Ok(child)
+ }
+
+ fn removed_file<P>(&self, path: P) -> miette::Result<Child<'_>>
+ where
+ P: AsRef<Path>,
+ {
+ let path = path.as_ref();
+ let child = self.child(path);
+ if child.exists() {
+ log::info!("removing old copy of {child}…",);
+ fs::remove_file(&*child)
+ .map_err(miette::Report::msg)
+ .wrap_err_with(|| format!("failed to remove old copy of {child}"))?;
+ }
+ Ok(child)
+ }
+
+ pub(crate) fn regen_dir<P>(
+ &self,
+ path: P,
+ gen: impl FnOnce(&Child<'_>) -> miette::Result<()>,
+ ) -> miette::Result<Child<'_>>
+ where
+ P: AsRef<Path>,
+ {
+ let child = self.removed_dir(path)?;
+ gen(&child)?;
+ ensure!(
+ child.is_dir(),
+ "{} was not regenerated for an unknown reason",
+ child,
+ );
+ Ok(child)
+ }
+
+ pub(crate) fn regen_file<P>(
+ &self,
+ path: P,
+ gen: impl FnOnce(&Child<'_>) -> miette::Result<()>,
+ ) -> miette::Result<Child<'_>>
+ where
+ P: AsRef<Path>,
+ {
+ let child = self.removed_file(path)?;
+ gen(&child)?;
+ ensure!(
+ child.is_file(),
+ "{} was not regenerated for an unknown reason",
+ child,
+ );
+ Ok(child)
+ }
+}
+
+impl Deref for FileRoot {
+ type Target = Path;
+
+ fn deref(&self) -> &Self::Target {
+ &self.path
+ }
+}
+
+impl AsRef<Path> for FileRoot {
+ fn as_ref(&self) -> &Path {
+ &self.path
+ }
+}
+
+impl AsRef<OsStr> for FileRoot {
+ fn as_ref(&self) -> &OsStr {
+ self.path.as_os_str()
+ }
+}
+
+impl Display for FileRoot {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let Self { nickname, path } = self;
+ write!(f, "`{}` (AKA `<{nickname}>`)", path.display())
+ }
+}
+
+pub(crate) struct Child<'a> {
+ root: &'a FileRoot,
+ /// NOTE: This is always an absolute path that is a child of the `root`.
+ path: PathBuf,
+}
+
+impl Child<'_> {
+ pub(crate) fn relative_path(&self) -> &Path {
+ let Self { root, path } = self;
+ path.strip_prefix(root).unwrap()
+ }
+
+ pub(crate) fn try_child<P>(&self, path: P) -> Result<Self, StripPrefixError>
+ where
+ P: AsRef<Path>,
+ {
+ let child_path = path.as_ref();
+ let Self { root, path } = self;
+
+ if child_path.is_absolute() {
+ child_path.strip_prefix(path)?;
+ }
+ Ok(Child {
+ root,
+ path: path.join(child_path),
+ })
+ }
+
+ #[track_caller]
+ pub(crate) fn child<P>(&self, path: P) -> Child<'_>
+ where
+ P: AsRef<Path>,
+ {
+ self.try_child(path)
+ .into_diagnostic()
+ .wrap_err("invariant violation: `path` is absolute and not a child of this child")
+ .unwrap()
+ }
+}
+
+impl Deref for Child<'_> {
+ type Target = Path;
+
+ fn deref(&self) -> &Self::Target {
+ &self.path
+ }
+}
+
+impl AsRef<Path> for Child<'_> {
+ fn as_ref(&self) -> &Path {
+ &self.path
+ }
+}
+
+impl AsRef<OsStr> for Child<'_> {
+ fn as_ref(&self) -> &OsStr {
+ self.path.as_os_str()
+ }
+}
+
+impl Display for Child<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "`<{}>{}{}`",
+ self.root.nickname(),
+ std::path::MAIN_SEPARATOR,
+ self.relative_path().display()
+ )
+ }
+}
+
+pub(crate) fn existing_file<P>(path: P) -> P
+where
+ P: AsRef<Path>,
+{
+ let p = path.as_ref();
+ assert!(p.is_file(), "{p:?} does not exist as a file");
+ path
+}
+
+pub(crate) fn copy_dir<P, Q>(source: P, dest: Q) -> miette::Result<()>
+where
+ P: Display + AsRef<Path>,
+ Q: Display + AsRef<Path>,
+{
+ log::debug!(
+ "copy-merging directories from {} into {}",
+ source.as_ref().display(),
+ dest.as_ref().display(),
+ );
+ ::dircpy::copy_dir(&source, &dest)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to copy files from {source} to {dest}"))
+}
+
+pub(crate) fn read_to_string<P>(path: P) -> miette::Result<String>
+where
+ P: AsRef<Path>,
+{
+ fs::read_to_string(&path)
+ .into_diagnostic()
+ .wrap_err_with(|| {
+ format!(
+ "failed to read UTF-8 string from path {}",
+ path.as_ref().display()
+ )
+ })
+}
+
+pub(crate) fn copy<P1, P2>(from: P1, to: P2) -> miette::Result<u64>
+where
+ P1: AsRef<Path>,
+ P2: AsRef<Path>,
+{
+ fs::copy(&from, &to).into_diagnostic().wrap_err_with(|| {
+ format!(
+ "failed to copy {} to {}",
+ from.as_ref().display(),
+ to.as_ref().display()
+ )
+ })
+}
+
+pub(crate) fn create_dir_all<P>(path: P) -> miette::Result<()>
+where
+ P: AsRef<Path>,
+{
+ fs::create_dir_all(&path)
+ .into_diagnostic()
+ .wrap_err_with(|| {
+ format!(
+ "failed to create directories leading up to {}",
+ path.as_ref().display()
+ )
+ })
+}
+
+pub(crate) fn remove_file<P>(path: P) -> miette::Result<()>
+where
+ P: AsRef<Path>,
+{
+ fs::remove_file(&path)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to remove file at path {}", path.as_ref().display()))
+}
+
+pub(crate) fn write<P, C>(path: P, contents: C) -> miette::Result<()>
+where
+ P: AsRef<Path>,
+ C: AsRef<[u8]>,
+{
+ fs::write(&path, &contents)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to write to path {}", path.as_ref().display()))
+}
diff --git a/dom/webgpu/tests/cts/vendor/src/main.rs b/dom/webgpu/tests/cts/vendor/src/main.rs
new file mode 100644
index 0000000000..b9afa9e94a
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/src/main.rs
@@ -0,0 +1,458 @@
+use std::{
+ collections::BTreeSet,
+ env::{current_dir, set_current_dir},
+ num::NonZeroUsize,
+ path::{Path, PathBuf},
+ process::ExitCode,
+};
+
+use clap::Parser;
+use lets_find_up::{find_up_with, FindUpKind, FindUpOptions};
+use miette::{ensure, miette, Context, Diagnostic, IntoDiagnostic, Report, SourceSpan};
+use regex::Regex;
+
+use crate::{
+ fs::{copy_dir, create_dir_all, existing_file, remove_file, FileRoot},
+ path::join_path,
+ process::{which, EasyCommand},
+};
+
+mod fs;
+mod path;
+mod process;
+
+/// Vendor WebGPU CTS tests from a local Git checkout of [our `gpuweb/cts` fork].
+///
+/// WPT tests are generated into `testing/web-platform/mozilla/tests/webgpu/`. If the set of tests
+/// changes upstream, make sure that the generated output still matches up with test expectation
+/// metadata in `testing/web-platform/mozilla/meta/webgpu/`.
+///
+/// [our `gpuweb/cts` fork]: https://github.com/mozilla/gpuweb-cts
+#[derive(Debug, Parser)]
+struct CliArgs {
+ /// A path to the top-level directory of your WebGPU CTS checkout.
+ cts_checkout_path: PathBuf,
+ /// The maximum capacity for test variant chunks.
+ ///
+ /// This tools divides the large number of CTS tests generated by upstream and chunks them into
+ /// multiple files. It's important to use a number that does not cause tests to time out in
+ /// Taskcluster. The current default value has been empirically tested for only this criteria.
+ /// Note that the way tests are divided may change in the future.
+ ///
+ /// If you intend to change the value long-term, change the default here.
+ #[clap(long, default_value = "25")]
+ chunk_size: NonZeroUsize,
+}
+
+fn main() -> ExitCode {
+ env_logger::builder()
+ .filter_level(log::LevelFilter::Info)
+ .parse_default_env()
+ .init();
+
+ let args = CliArgs::parse();
+
+ match run(args) {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(e) => {
+ log::error!("{e:?}");
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn run(args: CliArgs) -> miette::Result<()> {
+ let CliArgs {
+ chunk_size,
+ cts_checkout_path,
+ } = args;
+
+ let orig_working_dir = current_dir().unwrap();
+
+ let cts_dir = join_path(["dom", "webgpu", "tests", "cts"]);
+ let cts_vendor_dir = join_path([&*cts_dir, "vendor".as_ref()]);
+ let gecko_ckt = {
+ let failed_find_hg_err = || {
+ miette!(
+ "failed to find a Mercurial repository (`.hgrc`) in any of current working \
+ directory and its parent directories"
+ )
+ };
+ let hg_root = {
+ let mut dir = find_up_with(
+ ".hg",
+ FindUpOptions {
+ cwd: Path::new("."),
+ kind: FindUpKind::Dir,
+ },
+ )
+ .map_err(Report::msg)
+ .wrap_err_with(failed_find_hg_err)?
+ .ok_or_else(failed_find_hg_err)?;
+ dir.pop();
+ dir
+ };
+
+ let root = FileRoot::new("gecko", &hg_root)?;
+ log::info!("detected Gecko repository root at {root}");
+
+ ensure!(
+ root.try_child(&orig_working_dir)
+ .map_or(false, |c| c.relative_path() == cts_vendor_dir),
+ "It is expected to run this tool from the root of its Cargo project, but this does \
+ not appear to have been done. Bailing."
+ );
+
+ root
+ };
+
+ let cts_vendor_dir = gecko_ckt.child(orig_working_dir.parent().unwrap());
+
+ let wpt_tests_dir = {
+ let child = gecko_ckt.child(join_path(["testing", "web-platform", "mozilla", "tests"]));
+ ensure!(
+ child.is_dir(),
+ "WPT tests dir ({}) does not appear to exist",
+ child,
+ );
+ child
+ };
+
+ let (cts_ckt_git_dir, cts_ckt) = {
+ let failed_find_git_err = || {
+ miette!(
+ "failed to find a Git repository (`.git` directory) in the provided path and all \
+ of its parent directories"
+ )
+ };
+ let git_dir = find_up_with(
+ ".git",
+ FindUpOptions {
+ cwd: &cts_checkout_path,
+ kind: FindUpKind::Dir,
+ },
+ )
+ .map_err(Report::msg)
+ .wrap_err_with(failed_find_git_err)?
+ .ok_or_else(failed_find_git_err)?;
+
+ let ckt = FileRoot::new("cts", git_dir.parent().unwrap())?;
+ log::debug!("detected CTS checkout root at {ckt}");
+ (git_dir, ckt)
+ };
+
+ let git_bin = which("git", "Git binary")?;
+ let npm_bin = which("npm", "NPM binary")?;
+
+ // XXX: It'd be nice to expose separate operations for copying in source and generating WPT
+ // cases from the vendored copy. Checks like these really only matter when updating source.
+ let ensure_no_child = |p1: &FileRoot, p2| {
+ ensure!(
+ p1.try_child(p2).is_err(),
+ "{p1} is a child path of {p2}, which is not supported"
+ );
+ Ok(())
+ };
+ ensure_no_child(&cts_ckt, &gecko_ckt)?;
+ ensure_no_child(&gecko_ckt, &cts_ckt)?;
+
+ log::info!("making a vendored copy of checked-in files from {cts_ckt}…",);
+ gecko_ckt.regen_file(
+ join_path([&*cts_dir, "checkout_commit.txt".as_ref()]),
+ |checkout_commit_file| {
+ let mut git_status_porcelain_cmd = EasyCommand::new(&git_bin, |cmd| {
+ cmd.args(["status", "--porcelain"])
+ .envs([("GIT_DIR", &*cts_ckt_git_dir), ("GIT_WORK_TREE", &*cts_ckt)])
+ });
+ log::info!(
+ " …ensuring the working tree and index are clean with \
+ {git_status_porcelain_cmd}…"
+ );
+ let git_status_porcelain_output = git_status_porcelain_cmd.just_stdout_utf8()?;
+ ensure!(
+ git_status_porcelain_output.is_empty(),
+ "expected a clean CTS working tree and index, but {}'s output was not empty; \
+ for reference, it was:\n\n{}",
+ git_status_porcelain_cmd,
+ git_status_porcelain_output,
+ );
+
+ gecko_ckt.regen_dir(&cts_vendor_dir.join("checkout"), |vendored_ckt_dir| {
+ log::info!(" …copying files tracked by Git to {vendored_ckt_dir}…");
+ let files_to_vendor = {
+ let mut git_ls_files_cmd = EasyCommand::new(&git_bin, |cmd| {
+ cmd.arg("ls-files").env("GIT_DIR", &cts_ckt_git_dir)
+ });
+ log::debug!(" …getting files to vendor from {git_ls_files_cmd}…");
+ let output = git_ls_files_cmd.just_stdout_utf8()?;
+ let mut files = output
+ .split_terminator('\n')
+ .map(PathBuf::from)
+ .collect::<BTreeSet<_>>();
+ log::trace!(" …files from {git_ls_files_cmd}: {files:#?}");
+
+ log::trace!(" …validating that files from Git repo still exist…");
+ let files_not_found = files
+ .iter()
+ .filter(|p| !cts_ckt.child(p).exists())
+ .collect::<Vec<_>>();
+ ensure!(
+ files_not_found.is_empty(),
+ "the following files were returned by `git ls-files`, but do not \
+ exist on disk: {:#?}",
+ files_not_found,
+ );
+
+ log::trace!(" …stripping files we actually don't want to vendor…");
+ let files_to_actually_not_vendor = [
+ // There's no reason to bring this over, and lots of reasons to not bring in
+ // security-sensitive content unless we have to.
+ "deploy_key.enc",
+ ]
+ .map(Path::new);
+ log::trace!(" …files we don't want: {files_to_actually_not_vendor:?}");
+ for path in files_to_actually_not_vendor {
+ ensure!(
+ files.remove(path),
+ "failed to remove {} from list of files to vendor; does it still \
+ exist?",
+ cts_ckt.child(path)
+ );
+ }
+ files
+ };
+
+ log::debug!(" …now doing the copying…");
+ for path in files_to_vendor {
+ let vendor_from_path = cts_ckt.child(&path);
+ let vendor_to_path = vendored_ckt_dir.child(&path);
+ if let Some(parent) = vendor_to_path.parent() {
+ create_dir_all(vendored_ckt_dir.child(parent))?;
+ }
+ log::trace!(" …copying {vendor_from_path} to {vendor_to_path}…");
+ fs::copy(&vendor_from_path, &vendor_to_path)?;
+ }
+
+ Ok(())
+ })?;
+
+ log::info!(" …writing commit ref pointed to by `HEAD` to {checkout_commit_file}…");
+ let mut git_rev_parse_head_cmd = EasyCommand::new(&git_bin, |cmd| {
+ cmd.args(["rev-parse", "HEAD"])
+ .env("GIT_DIR", &cts_ckt_git_dir)
+ });
+ log::trace!(" …getting output of {git_rev_parse_head_cmd}…");
+ fs::write(
+ checkout_commit_file,
+ git_rev_parse_head_cmd.just_stdout_utf8()?,
+ )
+ .wrap_err_with(|| format!("failed to write HEAD ref to {checkout_commit_file}"))
+ },
+ )?;
+
+ set_current_dir(&*cts_ckt)
+ .into_diagnostic()
+ .wrap_err("failed to change working directory to CTS checkout")?;
+ log::debug!("changed CWD to {cts_ckt}");
+
+ let mut npm_ci_cmd = EasyCommand::new(&npm_bin, |cmd| cmd.arg("ci"));
+ log::info!(
+ "ensuring a clean {} directory with {npm_ci_cmd}…",
+ cts_ckt.child("node_modules"),
+ );
+ npm_ci_cmd.spawn()?;
+
+ let out_dir = cts_ckt.regen_dir("out", |out_dir| {
+ let mut npm_run_standalone_cmd =
+ EasyCommand::new(&npm_bin, |cmd| cmd.args(["run", "standalone"]));
+ log::info!(
+ "generating standalone runner files into {out_dir} with {npm_run_standalone_cmd}…"
+ );
+ npm_run_standalone_cmd.spawn()
+ })?;
+
+ let out_wpt_dir = cts_ckt.regen_dir("out-wpt", |out_wpt_dir| {
+ let mut npm_run_wpt_cmd = EasyCommand::new(&npm_bin, |cmd| cmd.args(["run", "wpt"]));
+ log::info!("generating WPT test cases into {out_wpt_dir} with {npm_run_wpt_cmd}…");
+ npm_run_wpt_cmd.spawn()
+ })?;
+
+ let cts_https_html_path = out_wpt_dir.child("cts.https.html");
+ log::info!("refining the output of {cts_https_html_path} with `npm run gen_wpt_cts_html …`…");
+ EasyCommand::new(&npm_bin, |cmd| {
+ cmd.args(["run", "gen_wpt_cts_html"])
+ .arg(existing_file(&cts_https_html_path))
+ .args([
+ existing_file(cts_ckt.child(join_path([
+ "src",
+ "common",
+ "templates",
+ "cts.https.html",
+ ]))),
+ existing_file(cts_vendor_dir.child("arguments.txt")),
+ existing_file(cts_vendor_dir.child("myexpectations.txt")),
+ ])
+ .arg("")
+ })
+ .spawn()?;
+
+ log::info!("stealing standalone runtime files from {out_dir} for {out_wpt_dir}…");
+ for subdir in [
+ &["external"] as &[_],
+ &["common", "internal"],
+ &["common", "util"],
+ ]
+ .map(join_path)
+ {
+ let out_subdir = out_dir.child(&subdir);
+ let out_wpt_subdir = out_wpt_dir.child(subdir);
+ log::info!(" …copying from {out_subdir} to {out_wpt_subdir}…");
+ copy_dir(out_subdir, out_wpt_subdir)?
+ }
+ log::info!(" …done stealing!");
+
+ log::info!("analyzing {cts_https_html_path}…");
+ let cts_https_html_content = fs::read_to_string(&*cts_https_html_path)?;
+ let cts_boilerplate;
+ let cts_cases;
+ {
+ {
+ let (boilerplate, cases_start) = {
+ let cases_start_idx = cts_https_html_content
+ .find("<meta name=variant")
+ .ok_or_else(|| miette!("no test cases found; this is unexpected!"))?;
+ cts_https_html_content.split_at(cases_start_idx)
+ };
+
+ cts_boilerplate = {
+ if !boilerplate.is_empty() {
+ #[derive(Debug, Diagnostic, thiserror::Error)]
+ #[error("last character before test cases was not a newline; bug, or weird?")]
+ #[diagnostic(severity("warning"))]
+ struct Oops {
+ #[label(
+ "this character ({:?}) was expected to be a newline, so that the test \
+ spec. following it is on its own line",
+ source_code.chars().last().unwrap()
+ )]
+ span: SourceSpan,
+ #[source_code]
+ source_code: String,
+ }
+ ensure!(
+ boilerplate.ends_with('\n'),
+ Oops {
+ span: SourceSpan::from(0..boilerplate.len()),
+ source_code: cts_https_html_content,
+ }
+ );
+ }
+ // NOTE: Adding `_mozilla` is necessary because [that's how it's mounted][source].
+ //
+ // [source]: https://searchfox.org/mozilla-central/rev/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/web-platform/mozilla/README#1-4]
+ log::info!(
+ " …fixing `script` paths in WPT boilerplate so they work as Mozilla-private \
+ WPT tests…"
+ );
+ let expected_wpt_script_tag =
+ "<script type=module src=/webgpu/common/runtime/wpt.js></script>";
+ ensure!(
+ boilerplate.contains(expected_wpt_script_tag),
+ "failed to find expected `script` tag for `wpt.js` \
+ ({:?}); did something change upstream?",
+ expected_wpt_script_tag
+ );
+ boilerplate.replacen(
+ expected_wpt_script_tag,
+ "<script type=module src=/_mozilla/webgpu/common/runtime/wpt.js></script>",
+ 1,
+ )
+ };
+
+ log::info!(" …parsing test variants in {cts_https_html_path}…");
+ cts_cases = cases_start.split_terminator('\n').collect::<Vec<_>>();
+ let mut parsing_failed = false;
+ let meta_variant_regex =
+ Regex::new("^<meta name=variant content='([^']*?)'>$").unwrap();
+ cts_cases.iter().for_each(|line| {
+ if !meta_variant_regex.is_match(line) {
+ parsing_failed = true;
+ log::error!("line is not a test case: {line:?}");
+ }
+ });
+ ensure!(
+ !parsing_failed,
+ "one or more test case lines failed to parse, fix it and try again"
+ );
+ };
+ log::trace!("\"original\" HTML boilerplate:\n\n{}", cts_boilerplate);
+
+ ensure!(
+ !cts_cases.is_empty(),
+ "no test cases found; this is unexpected!"
+ );
+ log::info!(" …found {} test cases", cts_cases.len());
+ }
+
+ cts_ckt.regen_dir(out_wpt_dir.join("chunked"), |chunked_tests_dir| {
+ // NOTE: We use an extremely simple chunking algorithm here. This was done in the name of
+ // speed of initial implementation. However, this might cause a significant amount of churn
+ // when tests get updated.
+ let chunks = cts_cases.chunks(chunk_size.get()).zip(1u32..);
+ log::info!(
+ "re-distributing tests into {} chunks of {chunk_size}…",
+ chunks.clone().count()
+ );
+ let mut failed_writing = false;
+ for (chunk, chunk_idx) in chunks {
+ // NOTE: Using `0`-padding here was considered, but it's probably not worth it. That
+ // would be in conflict with stable file paths as the set of tests grows.
+ let chunk_dir = chunked_tests_dir.child(chunk_idx.to_string());
+ match create_dir_all(&chunk_dir) {
+ Ok(()) => log::trace!("made directory {}", chunk_dir.display()),
+ Err(e) => {
+ failed_writing = true;
+ log::error!("{e:#}");
+ continue;
+ }
+ }
+ let chunk_file_path = chunk_dir.child("cts.https.html");
+ let chunk_file_content = {
+ let mut content = cts_boilerplate.as_bytes().to_vec();
+ for line in chunk {
+ content.extend(line.as_bytes());
+ content.extend(b"\n");
+ }
+ content
+ };
+ match fs::write(&chunk_file_path, &chunk_file_content).wrap_err_with(|| {
+ miette!("failed to write chunked output to path {chunk_file_path}")
+ }) {
+ Ok(()) => log::debug!(" …wrote {chunk_file_path}"),
+ Err(e) => {
+ failed_writing = true;
+ log::error!("{e:#}");
+ }
+ }
+ }
+ ensure!(
+ !failed_writing,
+ "failed to write one or more chunked WPT test files; see above output for more details"
+ );
+ log::debug!(" …finished writing new chunked WPT test files!");
+
+ log::info!(" …removing {cts_https_html_path}, now that it's been divided into chunks…");
+ remove_file(&cts_https_html_path)?;
+
+ Ok(())
+ })?;
+
+ gecko_ckt.regen_dir(wpt_tests_dir.join("webgpu"), |wpt_webgpu_tests_dir| {
+ log::info!("copying contents of {out_wpt_dir} to {wpt_webgpu_tests_dir}…");
+ copy_dir(&out_wpt_dir, wpt_webgpu_tests_dir)
+ })?;
+
+ log::info!("All done! Now get your CTS _ON_! :)");
+
+ Ok(())
+}
diff --git a/dom/webgpu/tests/cts/vendor/src/path.rs b/dom/webgpu/tests/cts/vendor/src/path.rs
new file mode 100644
index 0000000000..aa5bae2e6d
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/src/path.rs
@@ -0,0 +1,23 @@
+use std::path::{Path, PathBuf};
+
+/// Construct a [`PathBuf`] from individual [`Path`] components.
+///
+/// This is a simple and legible way to construct `PathBuf`s that use the system's native path
+/// separator character. (It's ugly to see paths mixing `\` and `/`.)
+///
+/// # Examples
+///
+/// ```rust
+/// # use std::path::Path;
+/// # use vendor_webgpu_cts::path::join_path;
+/// assert_eq!(&*join_path(["foo", "bar", "baz"]), Path::new("foo/bar/baz"));
+/// ```
+pub(crate) fn join_path<I, P>(iter: I) -> PathBuf
+where
+ I: IntoIterator<Item = P>,
+ P: AsRef<Path>,
+{
+ let mut path = PathBuf::new();
+ path.extend(iter);
+ path
+}
diff --git a/dom/webgpu/tests/cts/vendor/src/process.rs b/dom/webgpu/tests/cts/vendor/src/process.rs
new file mode 100644
index 0000000000..b36c3b953d
--- /dev/null
+++ b/dom/webgpu/tests/cts/vendor/src/process.rs
@@ -0,0 +1,85 @@
+use std::{
+ ffi::{OsStr, OsString},
+ fmt::{self, Display},
+ iter::once,
+ process::{Command, Output},
+};
+
+use format::lazy_format;
+use miette::{ensure, Context, IntoDiagnostic};
+
+pub(crate) fn which(name: &'static str, desc: &str) -> miette::Result<OsString> {
+ let found = ::which::which(name)
+ .into_diagnostic()
+ .wrap_err(lazy_format!("failed to find `{name}` executable"))?;
+ log::debug!("using {desc} from {}", found.display());
+ Ok(found.file_name().unwrap().to_owned())
+}
+
+pub(crate) struct EasyCommand {
+ inner: Command,
+}
+
+impl EasyCommand {
+ pub(crate) fn new<C>(cmd: C, f: impl FnOnce(&mut Command) -> &mut Command) -> Self
+ where
+ C: AsRef<OsStr>,
+ {
+ let mut cmd = Command::new(cmd);
+ f(&mut cmd);
+ Self { inner: cmd }
+ }
+
+ pub(crate) fn spawn(&mut self) -> miette::Result<()> {
+ log::debug!("spawning {self}…");
+ let status = self
+ .inner
+ .spawn()
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to spawn {self}"))?
+ .wait()
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to wait for exit code from {self}"))?;
+ log::debug!("{self} returned {:?}", status.code());
+ ensure!(status.success(), "{self} returned {:?}", status.code());
+ Ok(())
+ }
+
+ fn just_stdout(&mut self) -> miette::Result<Vec<u8>> {
+ log::debug!("getting `stdout` output of {self}");
+ let output = self
+ .inner
+ .output()
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to execute `{self}`"))?;
+ let Output {
+ status,
+ stdout: _,
+ stderr,
+ } = &output;
+ log::debug!("{self} returned {:?}", status.code());
+ ensure!(
+ status.success(),
+ "{self} returned {:?}; full output: {output:#?}",
+ status.code(),
+ );
+ assert!(stderr.is_empty());
+ Ok(output.stdout)
+ }
+
+ pub(crate) fn just_stdout_utf8(&mut self) -> miette::Result<String> {
+ String::from_utf8(self.just_stdout()?)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("output of {self} was not UTF-8 (!?)"))
+ }
+}
+
+impl Display for EasyCommand {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let Self { inner } = self;
+ let prog = inner.get_program().to_string_lossy();
+ let args = inner.get_args().map(|a| a.to_string_lossy());
+ let shell_words = ::shell_words::join(once(prog).chain(args));
+ write!(f, "`{shell_words}`")
+ }
+}