// Copyright 2017 The Dawn Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "dawn/native/BindGroupLayout.h" #include #include #include #include #include #include "dawn/common/BitSetIterator.h" #include "dawn/native/ChainUtils_autogen.h" #include "dawn/native/Device.h" #include "dawn/native/ObjectBase.h" #include "dawn/native/ObjectContentHasher.h" #include "dawn/native/ObjectType_autogen.h" #include "dawn/native/PerStage.h" #include "dawn/native/ValidationUtils_autogen.h" namespace dawn::native { namespace { MaybeError ValidateStorageTextureFormat(DeviceBase* device, wgpu::TextureFormat storageTextureFormat) { const Format* format = nullptr; DAWN_TRY_ASSIGN(format, device->GetInternalFormat(storageTextureFormat)); ASSERT(format != nullptr); DAWN_INVALID_IF(!format->supportsStorageUsage, "Texture format (%s) does not support storage textures.", storageTextureFormat); return {}; } MaybeError ValidateStorageTextureViewDimension(wgpu::TextureViewDimension dimension) { switch (dimension) { case wgpu::TextureViewDimension::Cube: case wgpu::TextureViewDimension::CubeArray: return DAWN_FORMAT_VALIDATION_ERROR( "%s texture views cannot be used as storage textures.", dimension); case wgpu::TextureViewDimension::e1D: case wgpu::TextureViewDimension::e2D: case wgpu::TextureViewDimension::e2DArray: case wgpu::TextureViewDimension::e3D: return {}; case wgpu::TextureViewDimension::Undefined: break; } UNREACHABLE(); } MaybeError ValidateBindGroupLayoutEntry(DeviceBase* device, const BindGroupLayoutEntry& entry, bool allowInternalBinding) { DAWN_TRY(ValidateShaderStage(entry.visibility)); int bindingMemberCount = 0; BindingInfoType bindingType; wgpu::ShaderStage allowedStages = kAllStages; if (entry.buffer.type != wgpu::BufferBindingType::Undefined) { bindingMemberCount++; bindingType = BindingInfoType::Buffer; const BufferBindingLayout& buffer = entry.buffer; // The kInternalStorageBufferBinding is used internally and not a value // in wgpu::BufferBindingType. if (buffer.type == kInternalStorageBufferBinding) { DAWN_INVALID_IF(!allowInternalBinding, "Internal binding types are disallowed"); } else { DAWN_TRY(ValidateBufferBindingType(buffer.type)); } if (buffer.type == wgpu::BufferBindingType::Storage || buffer.type == kInternalStorageBufferBinding) { allowedStages &= ~wgpu::ShaderStage::Vertex; } } if (entry.sampler.type != wgpu::SamplerBindingType::Undefined) { bindingMemberCount++; bindingType = BindingInfoType::Sampler; DAWN_TRY(ValidateSamplerBindingType(entry.sampler.type)); } if (entry.texture.sampleType != wgpu::TextureSampleType::Undefined) { bindingMemberCount++; bindingType = BindingInfoType::Texture; const TextureBindingLayout& texture = entry.texture; DAWN_TRY(ValidateTextureSampleType(texture.sampleType)); // viewDimension defaults to 2D if left undefined, needs validation otherwise. wgpu::TextureViewDimension viewDimension = wgpu::TextureViewDimension::e2D; if (texture.viewDimension != wgpu::TextureViewDimension::Undefined) { DAWN_TRY(ValidateTextureViewDimension(texture.viewDimension)); viewDimension = texture.viewDimension; } DAWN_INVALID_IF( texture.multisampled && viewDimension != wgpu::TextureViewDimension::e2D, "View dimension (%s) for a multisampled texture bindings was not %s.", viewDimension, wgpu::TextureViewDimension::e2D); } if (entry.storageTexture.access != wgpu::StorageTextureAccess::Undefined) { bindingMemberCount++; bindingType = BindingInfoType::StorageTexture; const StorageTextureBindingLayout& storageTexture = entry.storageTexture; DAWN_TRY(ValidateStorageTextureAccess(storageTexture.access)); DAWN_TRY(ValidateStorageTextureFormat(device, storageTexture.format)); // viewDimension defaults to 2D if left undefined, needs validation otherwise. if (storageTexture.viewDimension != wgpu::TextureViewDimension::Undefined) { DAWN_TRY(ValidateTextureViewDimension(storageTexture.viewDimension)); DAWN_TRY(ValidateStorageTextureViewDimension(storageTexture.viewDimension)); } if (storageTexture.access == wgpu::StorageTextureAccess::WriteOnly) { allowedStages &= ~wgpu::ShaderStage::Vertex; } } const ExternalTextureBindingLayout* externalTextureBindingLayout = nullptr; FindInChain(entry.nextInChain, &externalTextureBindingLayout); if (externalTextureBindingLayout != nullptr) { bindingMemberCount++; bindingType = BindingInfoType::ExternalTexture; } DAWN_INVALID_IF(bindingMemberCount == 0, "BindGroupLayoutEntry had none of buffer, sampler, texture, " "storageTexture, or externalTexture set"); DAWN_INVALID_IF(bindingMemberCount != 1, "BindGroupLayoutEntry had more than one of buffer, sampler, texture, " "storageTexture, or externalTexture set"); DAWN_INVALID_IF( !IsSubset(entry.visibility, allowedStages), "%s bindings cannot be used with a visibility of %s. Only %s are allowed.", bindingType, entry.visibility, allowedStages); return {}; } BindGroupLayoutEntry CreateSampledTextureBindingForExternalTexture( uint32_t binding, wgpu::ShaderStage visibility) { BindGroupLayoutEntry entry; entry.binding = binding; entry.visibility = visibility; entry.texture.viewDimension = wgpu::TextureViewDimension::e2D; entry.texture.multisampled = false; entry.texture.sampleType = wgpu::TextureSampleType::Float; return entry; } BindGroupLayoutEntry CreateUniformBindingForExternalTexture(uint32_t binding, wgpu::ShaderStage visibility) { BindGroupLayoutEntry entry; entry.binding = binding; entry.visibility = visibility; entry.buffer.hasDynamicOffset = false; entry.buffer.type = wgpu::BufferBindingType::Uniform; return entry; } std::vector ExtractAndExpandBglEntries( const BindGroupLayoutDescriptor* descriptor, BindingCounts* bindingCounts, ExternalTextureBindingExpansionMap* externalTextureBindingExpansions) { std::vector expandedOutput; // When new bgl entries are created, we use binding numbers larger than // kMaxBindingNumber to ensure there are no collisions. uint32_t nextOpenBindingNumberForNewEntry = kMaxBindingNumber + 1; for (uint32_t i = 0; i < descriptor->entryCount; i++) { const BindGroupLayoutEntry& entry = descriptor->entries[i]; const ExternalTextureBindingLayout* externalTextureBindingLayout = nullptr; FindInChain(entry.nextInChain, &externalTextureBindingLayout); // External textures are expanded from a texture_external into two sampled texture // bindings and one uniform buffer binding. The original binding number is used // for the first sampled texture. if (externalTextureBindingLayout != nullptr) { for (SingleShaderStage stage : IterateStages(entry.visibility)) { // External textures are not fully implemented, which means that expanding // the external texture at this time will not occupy the same number of // binding slots as defined in the WebGPU specification. Here we prematurely // increment the binding counts for an additional sampled textures and a // sampler so that an external texture will occupy the correct number of // slots for correct validation of shader binding limits. // TODO(dawn:1082): Consider removing this and instead making a change to // the validation. constexpr uint32_t kUnimplementedSampledTexturesPerExternalTexture = 2; constexpr uint32_t kUnimplementedSamplersPerExternalTexture = 1; bindingCounts->perStage[stage].sampledTextureCount += kUnimplementedSampledTexturesPerExternalTexture; bindingCounts->perStage[stage].samplerCount += kUnimplementedSamplersPerExternalTexture; } dawn_native::ExternalTextureBindingExpansion bindingExpansion; BindGroupLayoutEntry plane0Entry = CreateSampledTextureBindingForExternalTexture(entry.binding, entry.visibility); bindingExpansion.plane0 = BindingNumber(plane0Entry.binding); expandedOutput.push_back(plane0Entry); BindGroupLayoutEntry plane1Entry = CreateSampledTextureBindingForExternalTexture( nextOpenBindingNumberForNewEntry++, entry.visibility); bindingExpansion.plane1 = BindingNumber(plane1Entry.binding); expandedOutput.push_back(plane1Entry); BindGroupLayoutEntry paramsEntry = CreateUniformBindingForExternalTexture( nextOpenBindingNumberForNewEntry++, entry.visibility); bindingExpansion.params = BindingNumber(paramsEntry.binding); expandedOutput.push_back(paramsEntry); externalTextureBindingExpansions->insert( {BindingNumber(entry.binding), bindingExpansion}); } else { expandedOutput.push_back(entry); } } return expandedOutput; } } // anonymous namespace MaybeError ValidateBindGroupLayoutDescriptor(DeviceBase* device, const BindGroupLayoutDescriptor* descriptor, bool allowInternalBinding) { DAWN_INVALID_IF(descriptor->nextInChain != nullptr, "nextInChain must be nullptr"); std::set bindingsSet; BindingCounts bindingCounts = {}; for (uint32_t i = 0; i < descriptor->entryCount; ++i) { const BindGroupLayoutEntry& entry = descriptor->entries[i]; BindingNumber bindingNumber = BindingNumber(entry.binding); DAWN_INVALID_IF(bindingNumber > kMaxBindingNumberTyped, "Binding number (%u) exceeds the maximum binding number (%u).", uint32_t(bindingNumber), uint32_t(kMaxBindingNumberTyped)); DAWN_INVALID_IF(bindingsSet.count(bindingNumber) != 0, "On entries[%u]: binding index (%u) was specified by a previous entry.", i, entry.binding); DAWN_TRY_CONTEXT(ValidateBindGroupLayoutEntry(device, entry, allowInternalBinding), "validating entries[%u]", i); IncrementBindingCounts(&bindingCounts, entry); bindingsSet.insert(bindingNumber); } DAWN_TRY_CONTEXT(ValidateBindingCounts(bindingCounts), "validating binding counts"); return {}; } namespace { bool operator!=(const BindingInfo& a, const BindingInfo& b) { if (a.visibility != b.visibility || a.bindingType != b.bindingType) { return true; } switch (a.bindingType) { case BindingInfoType::Buffer: return a.buffer.type != b.buffer.type || a.buffer.hasDynamicOffset != b.buffer.hasDynamicOffset || a.buffer.minBindingSize != b.buffer.minBindingSize; case BindingInfoType::Sampler: return a.sampler.type != b.sampler.type; case BindingInfoType::Texture: return a.texture.sampleType != b.texture.sampleType || a.texture.viewDimension != b.texture.viewDimension || a.texture.multisampled != b.texture.multisampled; case BindingInfoType::StorageTexture: return a.storageTexture.access != b.storageTexture.access || a.storageTexture.viewDimension != b.storageTexture.viewDimension || a.storageTexture.format != b.storageTexture.format; case BindingInfoType::ExternalTexture: return false; } UNREACHABLE(); } bool IsBufferBinding(const BindGroupLayoutEntry& binding) { return binding.buffer.type != wgpu::BufferBindingType::Undefined; } bool BindingHasDynamicOffset(const BindGroupLayoutEntry& binding) { if (binding.buffer.type != wgpu::BufferBindingType::Undefined) { return binding.buffer.hasDynamicOffset; } return false; } BindingInfo CreateBindGroupLayoutInfo(const BindGroupLayoutEntry& binding) { BindingInfo bindingInfo; bindingInfo.binding = BindingNumber(binding.binding); bindingInfo.visibility = binding.visibility; if (binding.buffer.type != wgpu::BufferBindingType::Undefined) { bindingInfo.bindingType = BindingInfoType::Buffer; bindingInfo.buffer = binding.buffer; } else if (binding.sampler.type != wgpu::SamplerBindingType::Undefined) { bindingInfo.bindingType = BindingInfoType::Sampler; bindingInfo.sampler = binding.sampler; } else if (binding.texture.sampleType != wgpu::TextureSampleType::Undefined) { bindingInfo.bindingType = BindingInfoType::Texture; bindingInfo.texture = binding.texture; if (binding.texture.viewDimension == wgpu::TextureViewDimension::Undefined) { bindingInfo.texture.viewDimension = wgpu::TextureViewDimension::e2D; } } else if (binding.storageTexture.access != wgpu::StorageTextureAccess::Undefined) { bindingInfo.bindingType = BindingInfoType::StorageTexture; bindingInfo.storageTexture = binding.storageTexture; if (binding.storageTexture.viewDimension == wgpu::TextureViewDimension::Undefined) { bindingInfo.storageTexture.viewDimension = wgpu::TextureViewDimension::e2D; } } else { const ExternalTextureBindingLayout* externalTextureBindingLayout = nullptr; FindInChain(binding.nextInChain, &externalTextureBindingLayout); if (externalTextureBindingLayout != nullptr) { bindingInfo.bindingType = BindingInfoType::ExternalTexture; } } return bindingInfo; } bool SortBindingsCompare(const BindGroupLayoutEntry& a, const BindGroupLayoutEntry& b) { const bool aIsBuffer = IsBufferBinding(a); const bool bIsBuffer = IsBufferBinding(b); if (aIsBuffer != bIsBuffer) { // Always place buffers first. return aIsBuffer; } if (aIsBuffer) { bool aHasDynamicOffset = BindingHasDynamicOffset(a); bool bHasDynamicOffset = BindingHasDynamicOffset(b); ASSERT(bIsBuffer); if (aHasDynamicOffset != bHasDynamicOffset) { // Buffers with dynamic offsets should come before those without. // This makes it easy to iterate over the dynamic buffer bindings // [0, dynamicBufferCount) during validation. return aHasDynamicOffset; } if (aHasDynamicOffset) { ASSERT(bHasDynamicOffset); ASSERT(a.binding != b.binding); // Above, we ensured that dynamic buffers are first. Now, ensure that // dynamic buffer bindings are in increasing order. This is because dynamic // buffer offsets are applied in increasing order of binding number. return a.binding < b.binding; } } // This applies some defaults and gives us a single value to check for the binding type. BindingInfo aInfo = CreateBindGroupLayoutInfo(a); BindingInfo bInfo = CreateBindGroupLayoutInfo(b); // Sort by type. if (aInfo.bindingType != bInfo.bindingType) { return aInfo.bindingType < bInfo.bindingType; } if (a.visibility != b.visibility) { return a.visibility < b.visibility; } switch (aInfo.bindingType) { case BindingInfoType::Buffer: if (aInfo.buffer.minBindingSize != bInfo.buffer.minBindingSize) { return aInfo.buffer.minBindingSize < bInfo.buffer.minBindingSize; } break; case BindingInfoType::Sampler: if (aInfo.sampler.type != bInfo.sampler.type) { return aInfo.sampler.type < bInfo.sampler.type; } break; case BindingInfoType::Texture: if (aInfo.texture.multisampled != bInfo.texture.multisampled) { return aInfo.texture.multisampled < bInfo.texture.multisampled; } if (aInfo.texture.viewDimension != bInfo.texture.viewDimension) { return aInfo.texture.viewDimension < bInfo.texture.viewDimension; } if (aInfo.texture.sampleType != bInfo.texture.sampleType) { return aInfo.texture.sampleType < bInfo.texture.sampleType; } break; case BindingInfoType::StorageTexture: if (aInfo.storageTexture.access != bInfo.storageTexture.access) { return aInfo.storageTexture.access < bInfo.storageTexture.access; } if (aInfo.storageTexture.viewDimension != bInfo.storageTexture.viewDimension) { return aInfo.storageTexture.viewDimension < bInfo.storageTexture.viewDimension; } if (aInfo.storageTexture.format != bInfo.storageTexture.format) { return aInfo.storageTexture.format < bInfo.storageTexture.format; } break; case BindingInfoType::ExternalTexture: break; } return a.binding < b.binding; } // This is a utility function to help ASSERT that the BGL-binding comparator places buffers // first. bool CheckBufferBindingsFirst(ityp::span bindings) { BindingIndex lastBufferIndex{0}; BindingIndex firstNonBufferIndex = std::numeric_limits::max(); for (BindingIndex i{0}; i < bindings.size(); ++i) { if (bindings[i].bindingType == BindingInfoType::Buffer) { lastBufferIndex = std::max(i, lastBufferIndex); } else { firstNonBufferIndex = std::min(i, firstNonBufferIndex); } } // If there are no buffers, then |lastBufferIndex| is initialized to 0 and // |firstNonBufferIndex| gets set to 0. return firstNonBufferIndex >= lastBufferIndex; } } // namespace // BindGroupLayoutBase BindGroupLayoutBase::BindGroupLayoutBase(DeviceBase* device, const BindGroupLayoutDescriptor* descriptor, PipelineCompatibilityToken pipelineCompatibilityToken, ApiObjectBase::UntrackedByDeviceTag tag) : ApiObjectBase(device, descriptor->label), mPipelineCompatibilityToken(pipelineCompatibilityToken), mUnexpandedBindingCount(descriptor->entryCount) { std::vector sortedBindings = ExtractAndExpandBglEntries( descriptor, &mBindingCounts, &mExternalTextureBindingExpansionMap); std::sort(sortedBindings.begin(), sortedBindings.end(), SortBindingsCompare); for (uint32_t i = 0; i < sortedBindings.size(); ++i) { const BindGroupLayoutEntry& binding = sortedBindings[static_cast(i)]; mBindingInfo.push_back(CreateBindGroupLayoutInfo(binding)); if (IsBufferBinding(binding)) { // Buffers must be contiguously packed at the start of the binding info. ASSERT(GetBufferCount() == BindingIndex(i)); } IncrementBindingCounts(&mBindingCounts, binding); const auto& [_, inserted] = mBindingMap.emplace(BindingNumber(binding.binding), i); ASSERT(inserted); } ASSERT(CheckBufferBindingsFirst({mBindingInfo.data(), GetBindingCount()})); ASSERT(mBindingInfo.size() <= kMaxBindingsPerPipelineLayoutTyped); } BindGroupLayoutBase::BindGroupLayoutBase(DeviceBase* device, const BindGroupLayoutDescriptor* descriptor, PipelineCompatibilityToken pipelineCompatibilityToken) : BindGroupLayoutBase(device, descriptor, pipelineCompatibilityToken, kUntrackedByDevice) { TrackInDevice(); } BindGroupLayoutBase::BindGroupLayoutBase(DeviceBase* device, ObjectBase::ErrorTag tag) : ApiObjectBase(device, tag) { } BindGroupLayoutBase::BindGroupLayoutBase(DeviceBase* device) : ApiObjectBase(device, kLabelNotImplemented) { TrackInDevice(); } BindGroupLayoutBase::~BindGroupLayoutBase() = default; void BindGroupLayoutBase::DestroyImpl() { if (IsCachedReference()) { // Do not uncache the actual cached object if we are a blueprint. GetDevice()->UncacheBindGroupLayout(this); } } // static BindGroupLayoutBase* BindGroupLayoutBase::MakeError(DeviceBase* device) { return new BindGroupLayoutBase(device, ObjectBase::kError); } ObjectType BindGroupLayoutBase::GetType() const { return ObjectType::BindGroupLayout; } const BindGroupLayoutBase::BindingMap& BindGroupLayoutBase::GetBindingMap() const { ASSERT(!IsError()); return mBindingMap; } bool BindGroupLayoutBase::HasBinding(BindingNumber bindingNumber) const { return mBindingMap.count(bindingNumber) != 0; } BindingIndex BindGroupLayoutBase::GetBindingIndex(BindingNumber bindingNumber) const { ASSERT(!IsError()); const auto& it = mBindingMap.find(bindingNumber); ASSERT(it != mBindingMap.end()); return it->second; } size_t BindGroupLayoutBase::ComputeContentHash() { ObjectContentHasher recorder; recorder.Record(mPipelineCompatibilityToken); // std::map is sorted by key, so two BGLs constructed in different orders // will still record the same. for (const auto [id, index] : mBindingMap) { recorder.Record(id, index); const BindingInfo& info = mBindingInfo[index]; recorder.Record(info.buffer.hasDynamicOffset, info.visibility, info.bindingType, info.buffer.type, info.buffer.minBindingSize, info.sampler.type, info.texture.sampleType, info.texture.viewDimension, info.texture.multisampled, info.storageTexture.access, info.storageTexture.format, info.storageTexture.viewDimension); } return recorder.GetContentHash(); } bool BindGroupLayoutBase::EqualityFunc::operator()(const BindGroupLayoutBase* a, const BindGroupLayoutBase* b) const { return a->IsLayoutEqual(b); } BindingIndex BindGroupLayoutBase::GetBindingCount() const { return mBindingInfo.size(); } BindingIndex BindGroupLayoutBase::GetBufferCount() const { return BindingIndex(mBindingCounts.bufferCount); } BindingIndex BindGroupLayoutBase::GetDynamicBufferCount() const { // This is a binding index because dynamic buffers are packed at the front of the binding // info. return static_cast(mBindingCounts.dynamicStorageBufferCount + mBindingCounts.dynamicUniformBufferCount); } uint32_t BindGroupLayoutBase::GetUnverifiedBufferCount() const { return mBindingCounts.unverifiedBufferCount; } uint32_t BindGroupLayoutBase::GetExternalTextureBindingCount() const { return mExternalTextureBindingExpansionMap.size(); } const BindingCounts& BindGroupLayoutBase::GetBindingCountInfo() const { return mBindingCounts; } const ExternalTextureBindingExpansionMap& BindGroupLayoutBase::GetExternalTextureBindingExpansionMap() const { return mExternalTextureBindingExpansionMap; } uint32_t BindGroupLayoutBase::GetUnexpandedBindingCount() const { return mUnexpandedBindingCount; } bool BindGroupLayoutBase::IsLayoutEqual(const BindGroupLayoutBase* other, bool excludePipelineCompatibiltyToken) const { if (!excludePipelineCompatibiltyToken && GetPipelineCompatibilityToken() != other->GetPipelineCompatibilityToken()) { return false; } if (GetBindingCount() != other->GetBindingCount()) { return false; } for (BindingIndex i{0}; i < GetBindingCount(); ++i) { if (mBindingInfo[i] != other->mBindingInfo[i]) { return false; } } return mBindingMap == other->mBindingMap; } PipelineCompatibilityToken BindGroupLayoutBase::GetPipelineCompatibilityToken() const { return mPipelineCompatibilityToken; } size_t BindGroupLayoutBase::GetBindingDataSize() const { // | ------ buffer-specific ----------| ------------ object pointers -------------| // | --- offsets + sizes -------------| --------------- Ref ----------| // Followed by: // |---------buffer size array--------| // |-uint64_t[mUnverifiedBufferCount]-| size_t objectPointerStart = mBindingCounts.bufferCount * sizeof(BufferBindingData); ASSERT(IsAligned(objectPointerStart, alignof(Ref))); size_t bufferSizeArrayStart = Align(objectPointerStart + mBindingCounts.totalCount * sizeof(Ref), sizeof(uint64_t)); ASSERT(IsAligned(bufferSizeArrayStart, alignof(uint64_t))); return bufferSizeArrayStart + mBindingCounts.unverifiedBufferCount * sizeof(uint64_t); } BindGroupLayoutBase::BindingDataPointers BindGroupLayoutBase::ComputeBindingDataPointers( void* dataStart) const { BufferBindingData* bufferData = reinterpret_cast(dataStart); auto bindings = reinterpret_cast*>(bufferData + mBindingCounts.bufferCount); uint64_t* unverifiedBufferSizes = AlignPtr( reinterpret_cast(bindings + mBindingCounts.totalCount), sizeof(uint64_t)); ASSERT(IsPtrAligned(bufferData, alignof(BufferBindingData))); ASSERT(IsPtrAligned(bindings, alignof(Ref))); ASSERT(IsPtrAligned(unverifiedBufferSizes, alignof(uint64_t))); return {{bufferData, GetBufferCount()}, {bindings, GetBindingCount()}, {unverifiedBufferSizes, mBindingCounts.unverifiedBufferCount}}; } bool BindGroupLayoutBase::IsStorageBufferBinding(BindingIndex bindingIndex) const { ASSERT(bindingIndex < GetBufferCount()); switch (GetBindingInfo(bindingIndex).buffer.type) { case wgpu::BufferBindingType::Uniform: return false; case kInternalStorageBufferBinding: case wgpu::BufferBindingType::Storage: case wgpu::BufferBindingType::ReadOnlyStorage: return true; case wgpu::BufferBindingType::Undefined: UNREACHABLE(); } } std::string BindGroupLayoutBase::EntriesToString() const { std::string entries = "["; std::string sep = ""; const BindGroupLayoutBase::BindingMap& bindingMap = GetBindingMap(); for (const auto [bindingNumber, bindingIndex] : bindingMap) { const BindingInfo& bindingInfo = GetBindingInfo(bindingIndex); entries += absl::StrFormat("%s%s", sep, bindingInfo); sep = ", "; } entries += "]"; return entries; } } // namespace dawn::native