| // Copyright 2026 The Dawn & Tint Authors | |
| // | |
| // 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. | |
| #include <filesystem> | |
| #include <iostream> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <system_error> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/dawn/node/Module.h" | |
| #include "src/dawn/node/napi_v8/napi_v8.h" | |
| #include "src/dawn/node/standalone/EventLoop.h" | |
| #include "src/dawn/node/standalone/Polyfills.h" | |
| #include "src/dawn/utils/CommandLineParser.h" | |
| #include "libplatform/libplatform.h" | |
| #if defined(DAWN_ENABLE_PARTITION_ALLOC) | |
| #include "partition_alloc/buildflags.h" // nogncheck | |
| #if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) | |
| #include "partition_alloc/partition_alloc_constants.h" // nogncheck | |
| #include "partition_alloc/partition_root.h" // nogncheck | |
| #include "partition_alloc/shim/allocator_shim.h" // nogncheck | |
| #include "partition_alloc/shim/allocator_shim_default_dispatch_to_partition_alloc.h" // nogncheck | |
| #include "partition_alloc/thread_cache.h" // nogncheck | |
| #endif // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) | |
| #endif // defined(DAWN_ENABLE_PARTITION_ALLOC) | |
| namespace { | |
| // Relative path from the CTS directory to its command-line entry point. Must start with `./` so | |
| // the CommonJS `require()` polyfill resolves it as a relative file path rather than rejecting it | |
| // as a bare package name. | |
| constexpr const char* kCmdlineScript = "./out-node/common/runtime/cmdline.js"; | |
| class StandaloneRunner { | |
| public: | |
| int Run(int argc, const char* argv[]) { | |
| if (std::optional<int> exit_code = ParseCommandLine(argc, argv)) { | |
| return *exit_code; | |
| } | |
| ConfigurePartitionAlloc(); | |
| InitializeV8(); | |
| int exit_code = RunCts(); | |
| DisposeV8(); | |
| return exit_code; | |
| } | |
| private: | |
| // Parses command-line flags and switches the working directory to `--cts-dir`. Returns an exit | |
| // code if the process should terminate early (0 for `--help`, 1 on error), or `std::nullopt` | |
| // to continue running the CTS. | |
| std::optional<int> ParseCommandLine(int argc, const char* argv[]) { | |
| auto parse_result = parser_.Parse(argc, argv); | |
| if (!parse_result.success) { | |
| std::cerr << parse_result.errorMessage << "\n"; | |
| return 1; | |
| } | |
| if (help_opt_.GetValue()) { | |
| std::cout << "Usage: " << argv[0] << " [options] --query <query>\n\nOptions:\n"; | |
| parser_.PrintHelp(std::cout); | |
| return 0; | |
| } | |
| if (!query_opt_.IsSet()) { | |
| std::cerr << "Error: --query (-q) must be specified.\n\n"; | |
| std::cerr << "Usage: " << argv[0] << " [options] --query <query>\n\nOptions:\n"; | |
| parser_.PrintHelp(std::cerr); | |
| return 1; | |
| } | |
| // The CTS `cmdline.js` script requires the process working directory to be the root of the | |
| // CTS checkout: it checks that `src/common/runtime/cmdline.ts` exists relative to the | |
| // current working directory and loads test resources from `out-node/resources`. | |
| std::string cts_dir = | |
| cts_dir_opt_.IsSet() ? cts_dir_opt_.GetValue() : "third_party/webgpu-cts"; | |
| std::error_code ec; | |
| std::filesystem::current_path(cts_dir, ec); | |
| if (ec) { | |
| std::cerr << "Error: failed to change directory to '" << cts_dir | |
| << "': " << ec.message() << "\n"; | |
| return 1; | |
| } | |
| if (!std::filesystem::exists("src/common/runtime/cmdline.ts")) { | |
| std::cerr << "Error: '" << cts_dir | |
| << "' is not a WebGPU CTS checkout (missing 'src/common/runtime/cmdline.ts')." | |
| " Use --cts-dir to specify the path to the WebGPU CTS repository.\n"; | |
| return 1; | |
| } | |
| if (!std::filesystem::exists(kCmdlineScript) || | |
| !std::filesystem::exists("out-node/resources")) { | |
| std::cerr << "Error: the WebGPU CTS in '" << cts_dir | |
| << "' has not been built (missing 'out-node/common/runtime/cmdline.js' or " | |
| "'out-node/resources'). Build it by running 'npm ci && npx grunt node' in " | |
| "'" | |
| << cts_dir << "'.\n"; | |
| return 1; | |
| } | |
| return std::nullopt; | |
| } | |
| // Builds the `process.argv` array expected by the CTS `cmdline.js` script from the parsed | |
| // command-line options. | |
| std::vector<std::string> BuildJsArgs() const { | |
| std::vector<std::string> js_args = { | |
| "dawn_standalone_cts_runner", | |
| kCmdlineScript, | |
| "--gpu-provider", | |
| "dawn.node", | |
| }; | |
| const dawn::utils::CommandLineParser::BoolOption* bool_forwarding_opts[] = { | |
| &compat_opt_, | |
| &verbose_opt_, | |
| &list_opt_, | |
| &debug_opt_, | |
| &quiet_opt_, | |
| &unroll_const_eval_loops_opt_, | |
| &enforce_default_limits_opt_, | |
| &block_all_features_opt_, | |
| }; | |
| for (const auto* opt : bool_forwarding_opts) { | |
| if (opt->GetValue()) { | |
| js_args.push_back("--" + opt->GetName()); | |
| } | |
| } | |
| // CommandLineParser::StringListOption splits on ',', so rejoin comma-separated values that | |
| // belong to the preceding <key>=<value> flag. | |
| std::vector<std::string> dawn_flags; | |
| for (const std::string& token : dawn_flags_opt_.GetValue()) { | |
| if (token.find('=') != std::string::npos || dawn_flags.empty()) { | |
| dawn_flags.push_back(token); | |
| } else { | |
| dawn_flags.back() += "," + token; | |
| } | |
| } | |
| for (const std::string& flag : dawn_flags) { | |
| js_args.push_back("--gpu-provider-flag"); | |
| js_args.push_back(flag); | |
| } | |
| js_args.push_back(query_opt_.GetValue()); | |
| return js_args; | |
| } | |
| // Configures PartitionAlloc's malloc partition for a command-line test runner. For background | |
| // on buckets, slot spans, the empty slot span ring, and the thread cache, see: | |
| // https://chromium.googlesource.com/chromium/src/+/main/base/allocator/partition_allocator/PartitionAlloc.md | |
| void ConfigurePartitionAlloc() { | |
| #if defined(DAWN_ENABLE_PARTITION_ALLOC) | |
| #if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) | |
| // Use denser buckets (8 per power-of-two order instead of 4) to reduce internal | |
| // fragmentation, and disable security features (BackupRefPtr, MTE, quarantine, and zeroing | |
| // on free) that add per-free overhead. | |
| allocator_shim::ConfigurePartitions( | |
| allocator_shim::EnableBrp(false), /*brp_extra_extras_size=*/0, | |
| allocator_shim::EnableMemoryTagging(false), | |
| partition_alloc::TagViolationReportingMode::kDisabled, | |
| allocator_shim::BucketDistribution::kDenser, | |
| partition_alloc::internal::SchedulerLoopQuarantineConfig(), | |
| partition_alloc::internal::SchedulerLoopQuarantineConfig(), | |
| partition_alloc::internal::SchedulerLoopQuarantineConfig(), | |
| allocator_shim::EventuallyZeroFreedMemory(false)); | |
| // Enable the per-thread cache and raise its size limit from 512 B to 32 KiB | |
| // (`kThreadCacheLargeSizeThreshold`) so more allocations use the thread-local freelists. | |
| // This avoids acquiring a lock. | |
| partition_alloc::PartitionRoot* root = | |
| allocator_shim::internal::PartitionAllocMalloc::Allocator(); | |
| root->EnableThreadCacheIfSupported(); | |
| partition_alloc::ThreadCache::SetLargestCachedSize( | |
| partition_alloc::kThreadCacheLargeSizeThreshold); | |
| // Keep the maximum number of empty slot spans in the ring buffer before returning them to | |
| // the OS. | |
| root->AdjustSlotSpanRing( | |
| static_cast<int16_t>(partition_alloc::internal::kMaxEmptySlotSpanRingSize), | |
| /*dirty_bytes_shift=*/0); | |
| #endif // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) | |
| #endif // defined(DAWN_ENABLE_PARTITION_ALLOC) | |
| } | |
| void InitializeV8() { | |
| v8::V8::SetFlagsFromString("--expose_gc"); | |
| platform_ = v8::platform::NewDefaultPlatform(); | |
| v8::V8::InitializePlatform(platform_.get()); | |
| v8::V8::Initialize(); | |
| allocator_.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator()); | |
| v8::Isolate::CreateParams create_params; | |
| create_params.array_buffer_allocator = allocator_.get(); | |
| isolate_ = v8::Isolate::New(create_params); | |
| } | |
| void DisposeV8() { | |
| isolate_->Dispose(); | |
| isolate_ = nullptr; | |
| allocator_.reset(); | |
| v8::V8::Dispose(); | |
| v8::V8::DisposePlatform(); | |
| platform_.reset(); | |
| } | |
| void InitializePolyfills(Napi::Env env, dawn::node::standalone::EventLoop& loop) { | |
| dawn::node::standalone::PolyfillOptions polyfill_options; | |
| polyfill_options.argv = BuildJsArgs(); | |
| dawn::node::standalone::RegisterPolyfills(env, loop, polyfill_options); | |
| } | |
| // Initializes the built-in Dawn WebGPU module and exposes its WebGPU types on the JavaScript | |
| // global object. | |
| void InitializeWebGPU(Napi::Env env) { | |
| Napi::Object wgpu_exports = Napi::Object::New(env); | |
| Initialize(env, wgpu_exports); | |
| Napi::Object js_global = env.Global(); | |
| js_global.Set("_webgpu_module", wgpu_exports); | |
| // `Initialize()` exports the WebGPU constructors and namespace objects (`GPUBufferUsage`, | |
| // `GPUShaderStage`, `GPUValidationError`, etc.) on the `"globals"` property of | |
| // `wgpu_exports`. Expose each of those properties on `js_global`. | |
| Napi::Value wgpu_types_val = wgpu_exports.Get("globals"); | |
| if (wgpu_types_val.IsObject()) { | |
| Napi::Object wgpu_types = wgpu_types_val.As<Napi::Object>(); | |
| Napi::Array names = wgpu_types.GetPropertyNames(); | |
| for (uint32_t i = 0; i < names.Length(); ++i) { | |
| Napi::Value name = names.Get(i); | |
| js_global.Set(name, wgpu_types.Get(name)); | |
| } | |
| } | |
| } | |
| // Sets up the Node-API environment, loads the CTS command-line entry point, and runs the event | |
| // loop to completion. | |
| int RunCts() { | |
| int exit_code = 0; | |
| isolate_->Enter(); | |
| // `handle_scope` must be destroyed before `isolate_->Exit()`. | |
| { | |
| v8::HandleScope handle_scope(isolate_); | |
| v8::Local<v8::Context> context = v8::Context::New(isolate_); | |
| context->Enter(); | |
| napi_env c_env = dawn::napi_v8::CreateEnv(isolate_, context); | |
| // `loop` must be destroyed before `DestroyEnv(c_env)` because queued tasks hold | |
| // `Napi::Reference`s that unregister themselves from `c_env` on destruction. | |
| { | |
| Napi::Env env(c_env); | |
| dawn::node::standalone::EventLoop loop(isolate_, platform_.get()); | |
| // `InitializePolyfills` requires `loop` to bind timers and `process.exit`, and | |
| // must run before `InitializeWebGPU` which subclasses `Event` and `DOMException`. | |
| InitializePolyfills(env, loop); | |
| InitializeWebGPU(env); | |
| loop.PostTask([env] { | |
| Napi::Function require_fn = env.Global().Get("require").As<Napi::Function>(); | |
| require_fn.Call({Napi::String::New(env, kCmdlineScript)}); | |
| }); | |
| loop.Run(); | |
| exit_code = loop.exit_code(); | |
| } | |
| dawn::napi_v8::DestroyEnv(c_env); | |
| context->Exit(); | |
| } | |
| isolate_->Exit(); | |
| return exit_code; | |
| } | |
| dawn::utils::CommandLineParser parser_; | |
| dawn::utils::CommandLineParser::BoolOption& help_opt_ = parser_.AddHelp(); | |
| dawn::utils::CommandLineParser::StringOption& cts_dir_opt_ = | |
| parser_ | |
| .AddString("cts-dir", | |
| "Directory containing the WebGPU CTS (default: third_party/webgpu-cts)") | |
| .Parameter("dir"); | |
| dawn::utils::CommandLineParser::StringOption& query_opt_ = | |
| parser_.AddString("query", "CTS test query to run (e.g. 'webgpu:*')") | |
| .ShortName('q') | |
| .Parameter("query"); | |
| dawn::utils::CommandLineParser::BoolOption& verbose_opt_ = | |
| parser_.AddBool("verbose", "Print result/log of every test as it runs").ShortName('v'); | |
| dawn::utils::CommandLineParser::BoolOption& quiet_opt_ = | |
| parser_.AddBool("quiet", "Suppress summary information in output"); | |
| dawn::utils::CommandLineParser::BoolOption& list_opt_ = | |
| parser_.AddBool("list", "Print all testcase names that match the given query and exit"); | |
| dawn::utils::CommandLineParser::BoolOption& debug_opt_ = | |
| parser_.AddBool("debug", "Include debug messages in logging"); | |
| dawn::utils::CommandLineParser::BoolOption& compat_opt_ = | |
| parser_.AddBool("compat", "Run tests in compatibility mode"); | |
| dawn::utils::CommandLineParser::BoolOption& unroll_const_eval_loops_opt_ = | |
| parser_.AddBool("unroll-const-eval-loops", | |
| "Unroll loops in constant-evaluation shader execution tests"); | |
| dawn::utils::CommandLineParser::BoolOption& enforce_default_limits_opt_ = | |
| parser_.AddBool("enforce-default-limits", "Enforce the default limits"); | |
| dawn::utils::CommandLineParser::BoolOption& block_all_features_opt_ = | |
| parser_.AddBool("block-all-features", | |
| "Block all features (except 'core-features-and-limits')"); | |
| dawn::utils::CommandLineParser::StringListOption& dawn_flags_opt_ = | |
| parser_.AddStringList("dawn-flag", "Flag to set on dawn.node as <flag>=<value>") | |
| .Parameter("flag=value"); | |
| std::unique_ptr<v8::Platform> platform_; | |
| std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_; | |
| v8::Isolate* isolate_ = nullptr; | |
| }; | |
| } // namespace | |
| int main(int argc, const char* argv[]) { | |
| StandaloneRunner runner; | |
| return runner.Run(argc, argv); | |
| } |