// Copyright 2019 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/CommandValidation.h" #include "common/BitSetIterator.h" #include "dawn_native/BindGroup.h" #include "dawn_native/Buffer.h" #include "dawn_native/CommandBufferStateTracker.h" #include "dawn_native/Commands.h" #include "dawn_native/Device.h" #include "dawn_native/PassResourceUsage.h" #include "dawn_native/QuerySet.h" #include "dawn_native/RenderBundle.h" #include "dawn_native/RenderPipeline.h" namespace dawn_native { namespace { inline MaybeError ValidateRenderBundleCommand(CommandIterator* commands, Command type, CommandBufferStateTracker* commandBufferState, const AttachmentState* attachmentState, uint64_t* debugGroupStackSize, const char* disallowedMessage) { switch (type) { case Command::Draw: { commands->NextCommand(); DAWN_TRY(commandBufferState->ValidateCanDraw()); break; } case Command::DrawIndexed: { commands->NextCommand(); DAWN_TRY(commandBufferState->ValidateCanDrawIndexed()); break; } case Command::DrawIndirect: { commands->NextCommand(); DAWN_TRY(commandBufferState->ValidateCanDraw()); break; } case Command::DrawIndexedIndirect: { commands->NextCommand(); DAWN_TRY(commandBufferState->ValidateCanDrawIndexed()); break; } case Command::InsertDebugMarker: { InsertDebugMarkerCmd* cmd = commands->NextCommand(); commands->NextData(cmd->length + 1); break; } case Command::PopDebugGroup: { commands->NextCommand(); DAWN_TRY(ValidateCanPopDebugGroup(*debugGroupStackSize)); *debugGroupStackSize -= 1; break; } case Command::PushDebugGroup: { PushDebugGroupCmd* cmd = commands->NextCommand(); commands->NextData(cmd->length + 1); *debugGroupStackSize += 1; break; } case Command::SetRenderPipeline: { SetRenderPipelineCmd* cmd = commands->NextCommand(); RenderPipelineBase* pipeline = cmd->pipeline.Get(); if (DAWN_UNLIKELY(pipeline->GetAttachmentState() != attachmentState)) { return DAWN_VALIDATION_ERROR("Pipeline attachment state is not compatible"); } commandBufferState->SetRenderPipeline(pipeline); break; } case Command::SetBindGroup: { SetBindGroupCmd* cmd = commands->NextCommand(); if (cmd->dynamicOffsetCount > 0) { commands->NextData(cmd->dynamicOffsetCount); } commandBufferState->SetBindGroup(cmd->index, cmd->group.Get()); break; } case Command::SetIndexBuffer: { SetIndexBufferCmd* cmd = commands->NextCommand(); commandBufferState->SetIndexBuffer(cmd->format); break; } case Command::SetVertexBuffer: { SetVertexBufferCmd* cmd = commands->NextCommand(); commandBufferState->SetVertexBuffer(cmd->slot); break; } default: return DAWN_VALIDATION_ERROR(disallowedMessage); } return {}; } } // namespace MaybeError ValidateCanPopDebugGroup(uint64_t debugGroupStackSize) { if (debugGroupStackSize == 0) { return DAWN_VALIDATION_ERROR("Pop must be balanced by a corresponding Push."); } return {}; } MaybeError ValidateFinalDebugGroupStackSize(uint64_t debugGroupStackSize) { if (debugGroupStackSize != 0) { return DAWN_VALIDATION_ERROR("Each Push must be balanced by a corresponding Pop."); } return {}; } MaybeError ValidateRenderBundle(CommandIterator* commands, const AttachmentState* attachmentState) { CommandBufferStateTracker commandBufferState; uint64_t debugGroupStackSize = 0; Command type; while (commands->NextCommandId(&type)) { DAWN_TRY(ValidateRenderBundleCommand(commands, type, &commandBufferState, attachmentState, &debugGroupStackSize, "Command disallowed inside a render bundle")); } DAWN_TRY(ValidateFinalDebugGroupStackSize(debugGroupStackSize)); return {}; } MaybeError ValidateRenderPass(CommandIterator* commands, const BeginRenderPassCmd* renderPass) { CommandBufferStateTracker commandBufferState; uint64_t debugGroupStackSize = 0; Command type; while (commands->NextCommandId(&type)) { switch (type) { case Command::BeginOcclusionQuery: { commands->NextCommand(); break; } case Command::EndOcclusionQuery: { commands->NextCommand(); break; } case Command::EndRenderPass: { commands->NextCommand(); DAWN_TRY(ValidateFinalDebugGroupStackSize(debugGroupStackSize)); return {}; } case Command::ExecuteBundles: { ExecuteBundlesCmd* cmd = commands->NextCommand(); auto bundles = commands->NextData>(cmd->count); for (uint32_t i = 0; i < cmd->count; ++i) { if (DAWN_UNLIKELY(renderPass->attachmentState.Get() != bundles[i]->GetAttachmentState())) { return DAWN_VALIDATION_ERROR( "Render bundle is not compatible with render pass"); } } if (cmd->count > 0) { // Reset state. It is invalidated after render bundle execution. commandBufferState = CommandBufferStateTracker{}; } break; } case Command::SetStencilReference: { commands->NextCommand(); break; } case Command::SetBlendColor: { commands->NextCommand(); break; } case Command::SetViewport: { commands->NextCommand(); break; } case Command::SetScissorRect: { commands->NextCommand(); break; } case Command::WriteTimestamp: { commands->NextCommand(); break; } default: DAWN_TRY(ValidateRenderBundleCommand( commands, type, &commandBufferState, renderPass->attachmentState.Get(), &debugGroupStackSize, "Command disallowed inside a render pass")); } } UNREACHABLE(); return DAWN_VALIDATION_ERROR("Unfinished render pass"); } MaybeError ValidateComputePass(CommandIterator* commands) { CommandBufferStateTracker commandBufferState; uint64_t debugGroupStackSize = 0; Command type; while (commands->NextCommandId(&type)) { switch (type) { case Command::EndComputePass: { commands->NextCommand(); DAWN_TRY(ValidateFinalDebugGroupStackSize(debugGroupStackSize)); return {}; } case Command::Dispatch: { commands->NextCommand(); DAWN_TRY(commandBufferState.ValidateCanDispatch()); break; } case Command::DispatchIndirect: { commands->NextCommand(); DAWN_TRY(commandBufferState.ValidateCanDispatch()); break; } case Command::InsertDebugMarker: { InsertDebugMarkerCmd* cmd = commands->NextCommand(); commands->NextData(cmd->length + 1); break; } case Command::PopDebugGroup: { commands->NextCommand(); DAWN_TRY(ValidateCanPopDebugGroup(debugGroupStackSize)); debugGroupStackSize--; break; } case Command::PushDebugGroup: { PushDebugGroupCmd* cmd = commands->NextCommand(); commands->NextData(cmd->length + 1); debugGroupStackSize++; break; } case Command::SetComputePipeline: { SetComputePipelineCmd* cmd = commands->NextCommand(); ComputePipelineBase* pipeline = cmd->pipeline.Get(); commandBufferState.SetComputePipeline(pipeline); break; } case Command::SetBindGroup: { SetBindGroupCmd* cmd = commands->NextCommand(); if (cmd->dynamicOffsetCount > 0) { commands->NextData(cmd->dynamicOffsetCount); } commandBufferState.SetBindGroup(cmd->index, cmd->group.Get()); break; } case Command::WriteTimestamp: { commands->NextCommand(); break; } default: return DAWN_VALIDATION_ERROR("Command disallowed inside a compute pass"); } } UNREACHABLE(); return DAWN_VALIDATION_ERROR("Unfinished compute pass"); } // Performs the per-pass usage validation checks // This will eventually need to differentiate between render and compute passes. // It will be valid to use a buffer both as uniform and storage in the same compute pass. MaybeError ValidatePassResourceUsage(const PassResourceUsage& pass) { // Buffers can only be used as single-write or multiple read. for (size_t i = 0; i < pass.buffers.size(); ++i) { const BufferBase* buffer = pass.buffers[i]; wgpu::BufferUsage usage = pass.bufferUsages[i]; if (usage & ~buffer->GetUsage()) { return DAWN_VALIDATION_ERROR("Buffer missing usage for the pass"); } bool readOnly = IsSubset(usage, kReadOnlyBufferUsages); bool singleUse = wgpu::HasZeroOrOneBits(usage); if (pass.passType == PassType::Render && !readOnly && !singleUse) { return DAWN_VALIDATION_ERROR( "Buffer used as writable usage and another usage in pass"); } } // Textures can only be used as single-write or multiple read. for (size_t i = 0; i < pass.textures.size(); ++i) { const TextureBase* texture = pass.textures[i]; const PassTextureUsage& textureUsage = pass.textureUsages[i]; wgpu::TextureUsage usage = textureUsage.usage; if (usage & ~texture->GetUsage()) { return DAWN_VALIDATION_ERROR("Texture missing usage for the pass"); } // TODO (yunchao.he@intel.com): add read/write usage tracking for compute // The usage variable for the whole texture is a fast path for texture usage tracking. // Because in most cases a texture (with or without subresources) is used as // single-write or multiple read, then we can skip iterating the subresources' usages. bool readOnly = IsSubset(usage, kReadOnlyTextureUsages); bool singleUse = wgpu::HasZeroOrOneBits(usage); if (pass.passType != PassType::Render || readOnly || singleUse) { continue; } // Inspect the subresources if the usage of the whole texture violates usage validation. // Every single subresource can only be used as single-write or multiple read. for (wgpu::TextureUsage subresourceUsage : textureUsage.subresourceUsages) { bool readOnly = IsSubset(subresourceUsage, kReadOnlyTextureUsages); bool singleUse = wgpu::HasZeroOrOneBits(subresourceUsage); if (!readOnly && !singleUse) { return DAWN_VALIDATION_ERROR( "Texture used as writable usage and another usage in render pass"); } } } return {}; } MaybeError ValidateTimestampQuery(QuerySetBase* querySet, uint32_t queryIndex) { if (querySet->GetQueryType() != wgpu::QueryType::Timestamp) { return DAWN_VALIDATION_ERROR("The type of query set must be Timestamp"); } if (queryIndex >= querySet->GetQueryCount()) { return DAWN_VALIDATION_ERROR("Query index exceeds the number of queries in query set"); } return {}; } bool IsRangeOverlapped(uint32_t startA, uint32_t startB, uint32_t length) { uint32_t maxStart = std::max(startA, startB); uint32_t minStart = std::min(startA, startB); return static_cast(minStart) + static_cast(length) > static_cast(maxStart); } template DAWN_FORCE_INLINE uint64_t Safe32x32(A a, B b) { static_assert(std::is_same::value, "'a' must be uint32_t"); static_assert(std::is_same::value, "'b' must be uint32_t"); return uint64_t(a) * uint64_t(b); } ResultOrError ComputeRequiredBytesInCopy(const TexelBlockInfo& blockInfo, const Extent3D& copySize, uint32_t bytesPerRow, uint32_t rowsPerImage) { ASSERT(copySize.width % blockInfo.width == 0); ASSERT(copySize.height % blockInfo.height == 0); uint32_t widthInBlocks = copySize.width / blockInfo.width; uint32_t heightInBlocks = copySize.height / blockInfo.height; uint64_t bytesInLastRow = Safe32x32(widthInBlocks, blockInfo.byteSize); if (copySize.depth == 0) { return 0; } // Check for potential overflows for the rest of the computations. We have the following // inequalities: // // bytesInLastRow <= bytesPerRow // heightInBlocks <= rowsPerImage // // So: // // bytesInLastImage = bytesPerRow * (heightInBlocks - 1) + bytesInLastRow // <= bytesPerRow * heightInBlocks // <= bytesPerRow * rowsPerImage // <= bytesPerImage // // This means that if the computation of depth * bytesPerImage doesn't overflow, none of the // computations for requiredBytesInCopy will. (and it's not a very pessimizing check) ASSERT(copySize.depth <= 1 || (bytesPerRow != wgpu::kStrideUndefined && rowsPerImage != wgpu::kStrideUndefined)); uint64_t bytesPerImage = Safe32x32(bytesPerRow, rowsPerImage); if (bytesPerImage > std::numeric_limits::max() / copySize.depth) { return DAWN_VALIDATION_ERROR("requiredBytesInCopy is too large."); } uint64_t requiredBytesInCopy = bytesPerImage * (copySize.depth - 1); if (heightInBlocks > 0) { ASSERT(heightInBlocks <= 1 || bytesPerRow != wgpu::kStrideUndefined); uint64_t bytesInLastImage = Safe32x32(bytesPerRow, heightInBlocks - 1) + bytesInLastRow; requiredBytesInCopy += bytesInLastImage; } return requiredBytesInCopy; } MaybeError ValidateCopySizeFitsInBuffer(const Ref& buffer, uint64_t offset, uint64_t size) { uint64_t bufferSize = buffer->GetSize(); bool fitsInBuffer = offset <= bufferSize && (size <= (bufferSize - offset)); if (!fitsInBuffer) { return DAWN_VALIDATION_ERROR("Copy would overflow the buffer"); } return {}; } TextureDataLayout FixUpDeprecatedTextureDataLayoutOptions( DeviceBase* device, const TextureDataLayout& originalLayout, const TexelBlockInfo& blockInfo, const Extent3D& copyExtent) { // TODO(crbug.com/dawn/520): Remove deprecated functionality. TextureDataLayout layout = originalLayout; if (copyExtent.height != 0 && layout.rowsPerImage == 0) { if (copyExtent.depth > 1) { device->EmitDeprecationWarning( "rowsPerImage soon must be non-zero if copy depth > 1 (it will no longer " "default to the copy height)."); ASSERT(copyExtent.height % blockInfo.height == 0); uint32_t heightInBlocks = copyExtent.height / blockInfo.height; layout.rowsPerImage = heightInBlocks; } else if (copyExtent.depth == 1) { device->EmitDeprecationWarning( "rowsPerImage soon must be non-zero or unspecified if copy depth == 1 (it will " "no longer default to the copy height)."); layout.rowsPerImage = wgpu::kStrideUndefined; } } // Only bother to fix-up for height == 1 && depth == 1. // The other cases that used to be allowed were zero-size copies. ASSERT(copyExtent.width % blockInfo.width == 0); uint32_t widthInBlocks = copyExtent.width / blockInfo.width; uint32_t bytesInLastRow = widthInBlocks * blockInfo.byteSize; if (copyExtent.height == 1 && copyExtent.depth == 1 && bytesInLastRow > layout.bytesPerRow) { device->EmitDeprecationWarning( "Soon, even if copy height == 1, bytesPerRow must be >= the byte size of each row " "or left unspecified."); layout.bytesPerRow = wgpu::kStrideUndefined; } return layout; } // Replace wgpu::kStrideUndefined with real values, so backends don't have to think about it. void ApplyDefaultTextureDataLayoutOptions(TextureDataLayout* layout, const TexelBlockInfo& blockInfo, const Extent3D& copyExtent) { ASSERT(layout != nullptr); ASSERT(copyExtent.height % blockInfo.height == 0); uint32_t heightInBlocks = copyExtent.height / blockInfo.height; if (layout->bytesPerRow == wgpu::kStrideUndefined) { ASSERT(copyExtent.width % blockInfo.width == 0); uint32_t widthInBlocks = copyExtent.width / blockInfo.width; uint32_t bytesInLastRow = widthInBlocks * blockInfo.byteSize; ASSERT(heightInBlocks <= 1 && copyExtent.depth <= 1); layout->bytesPerRow = Align(bytesInLastRow, kTextureBytesPerRowAlignment); } if (layout->rowsPerImage == wgpu::kStrideUndefined) { ASSERT(copyExtent.depth <= 1); layout->rowsPerImage = heightInBlocks; } } MaybeError ValidateLinearTextureData(const TextureDataLayout& layout, uint64_t byteSize, const TexelBlockInfo& blockInfo, const Extent3D& copyExtent) { ASSERT(copyExtent.height % blockInfo.height == 0); uint32_t heightInBlocks = copyExtent.height / blockInfo.height; if (copyExtent.depth > 1 && (layout.bytesPerRow == wgpu::kStrideUndefined || layout.rowsPerImage == wgpu::kStrideUndefined)) { return DAWN_VALIDATION_ERROR( "If copy depth > 1, bytesPerRow and rowsPerImage must be specified."); } if (heightInBlocks > 1 && layout.bytesPerRow == wgpu::kStrideUndefined) { return DAWN_VALIDATION_ERROR("If heightInBlocks > 1, bytesPerRow must be specified."); } // Validation for other members in layout: ASSERT(copyExtent.width % blockInfo.width == 0); uint32_t widthInBlocks = copyExtent.width / blockInfo.width; ASSERT(Safe32x32(widthInBlocks, blockInfo.byteSize) <= std::numeric_limits::max()); uint32_t bytesInLastRow = widthInBlocks * blockInfo.byteSize; // These != wgpu::kStrideUndefined checks are technically redundant with the > checks, but // they should get optimized out. if (layout.bytesPerRow != wgpu::kStrideUndefined && bytesInLastRow > layout.bytesPerRow) { return DAWN_VALIDATION_ERROR("The byte size of each row must be <= bytesPerRow."); } if (layout.rowsPerImage != wgpu::kStrideUndefined && heightInBlocks > layout.rowsPerImage) { return DAWN_VALIDATION_ERROR( "The height of each image, in blocks, must be <= rowsPerImage."); } // We compute required bytes in copy after validating texel block alignments // because the divisibility conditions are necessary for the algorithm to be valid, // also the bytesPerRow bound is necessary to avoid overflows. uint64_t requiredBytesInCopy; DAWN_TRY_ASSIGN(requiredBytesInCopy, ComputeRequiredBytesInCopy(blockInfo, copyExtent, layout.bytesPerRow, layout.rowsPerImage)); bool fitsInData = layout.offset <= byteSize && (requiredBytesInCopy <= (byteSize - layout.offset)); if (!fitsInData) { return DAWN_VALIDATION_ERROR( "Required size for texture data layout exceeds the linear data size."); } return {}; } MaybeError ValidateBufferCopyView(DeviceBase const* device, const BufferCopyView& bufferCopyView) { DAWN_TRY(device->ValidateObject(bufferCopyView.buffer)); if (bufferCopyView.layout.bytesPerRow != wgpu::kStrideUndefined) { if (bufferCopyView.layout.bytesPerRow % kTextureBytesPerRowAlignment != 0) { return DAWN_VALIDATION_ERROR("bytesPerRow must be a multiple of 256"); } } return {}; } MaybeError ValidateTextureCopyView(DeviceBase const* device, const TextureCopyView& textureCopy, const Extent3D& copySize) { const TextureBase* texture = textureCopy.texture; DAWN_TRY(device->ValidateObject(texture)); if (textureCopy.mipLevel >= texture->GetNumMipLevels()) { return DAWN_VALIDATION_ERROR("mipLevel out of range"); } if (TryConvertAspect(texture->GetFormat(), textureCopy.aspect) == Aspect::None) { return DAWN_VALIDATION_ERROR("Texture does not have selected aspect for texture copy."); } if (texture->GetSampleCount() > 1 || texture->GetFormat().HasDepthOrStencil()) { Extent3D subresourceSize = texture->GetMipLevelPhysicalSize(textureCopy.mipLevel); ASSERT(texture->GetDimension() == wgpu::TextureDimension::e2D); if (textureCopy.origin.x != 0 || textureCopy.origin.y != 0 || subresourceSize.width != copySize.width || subresourceSize.height != copySize.height) { return DAWN_VALIDATION_ERROR( "The entire subresource must be copied when using a depth/stencil texture, or " "when sample count is greater than 1."); } } return {}; } MaybeError ValidateTextureCopyRange(const TextureCopyView& textureCopy, const Extent3D& copySize) { // TODO(jiawei.shao@intel.com): add validations on the texture-to-texture copies within the // same texture. const TextureBase* texture = textureCopy.texture; // Validation for the copy being in-bounds: Extent3D mipSize = texture->GetMipLevelPhysicalSize(textureCopy.mipLevel); // For 2D textures, include the array layer as depth so it can be checked with other // dimensions. ASSERT(texture->GetDimension() == wgpu::TextureDimension::e2D); mipSize.depth = texture->GetArrayLayers(); // All texture dimensions are in uint32_t so by doing checks in uint64_t we avoid // overflows. if (static_cast(textureCopy.origin.x) + static_cast(copySize.width) > static_cast(mipSize.width) || static_cast(textureCopy.origin.y) + static_cast(copySize.height) > static_cast(mipSize.height) || static_cast(textureCopy.origin.z) + static_cast(copySize.depth) > static_cast(mipSize.depth)) { return DAWN_VALIDATION_ERROR("Touching outside of the texture"); } // Validation for the texel block alignments: const Format& format = textureCopy.texture->GetFormat(); if (format.isCompressed) { const TexelBlockInfo& blockInfo = format.GetAspectInfo(textureCopy.aspect).block; if (textureCopy.origin.x % blockInfo.width != 0) { return DAWN_VALIDATION_ERROR( "Offset.x must be a multiple of compressed texture format block width"); } if (textureCopy.origin.y % blockInfo.height != 0) { return DAWN_VALIDATION_ERROR( "Offset.y must be a multiple of compressed texture format block height"); } if (copySize.width % blockInfo.width != 0) { return DAWN_VALIDATION_ERROR( "copySize.width must be a multiple of compressed texture format block width"); } if (copySize.height % blockInfo.height != 0) { return DAWN_VALIDATION_ERROR( "copySize.height must be a multiple of compressed texture format block height"); } } return {}; } // Always returns a single aspect (color, stencil, or depth). ResultOrError SingleAspectUsedByTextureCopyView(const TextureCopyView& view) { const Format& format = view.texture->GetFormat(); switch (view.aspect) { case wgpu::TextureAspect::All: if (HasOneBit(format.aspects)) { return Aspect{format.aspects}; } else { return DAWN_VALIDATION_ERROR( "A single aspect must be selected for multi-planar formats in " "texture <-> linear data copies"); } break; case wgpu::TextureAspect::DepthOnly: ASSERT(format.aspects & Aspect::Depth); return Aspect::Depth; case wgpu::TextureAspect::StencilOnly: ASSERT(format.aspects & Aspect::Stencil); return Aspect::Stencil; } } MaybeError ValidateLinearToDepthStencilCopyRestrictions(const TextureCopyView& dst) { Aspect aspectUsed; DAWN_TRY_ASSIGN(aspectUsed, SingleAspectUsedByTextureCopyView(dst)); if (aspectUsed == Aspect::Depth) { return DAWN_VALIDATION_ERROR("Cannot copy into the depth aspect of a texture"); } return {}; } MaybeError ValidateTextureToTextureCopyRestrictions(const TextureCopyView& src, const TextureCopyView& dst, const Extent3D& copySize) { const uint32_t srcSamples = src.texture->GetSampleCount(); const uint32_t dstSamples = dst.texture->GetSampleCount(); if (srcSamples != dstSamples) { return DAWN_VALIDATION_ERROR( "Source and destination textures must have matching sample counts."); } if (src.texture->GetFormat().format != dst.texture->GetFormat().format) { // Metal requires texture-to-texture copies be the same format return DAWN_VALIDATION_ERROR("Source and destination texture formats must match."); } if (src.aspect != wgpu::TextureAspect::All || dst.aspect != wgpu::TextureAspect::All) { // Metal cannot select a single aspect for texture-to-texture copies return DAWN_VALIDATION_ERROR( "Texture aspect must be \"all\" for texture to texture copies"); } if (src.texture == dst.texture && src.mipLevel == dst.mipLevel) { ASSERT(src.texture->GetDimension() == wgpu::TextureDimension::e2D && dst.texture->GetDimension() == wgpu::TextureDimension::e2D); if (IsRangeOverlapped(src.origin.z, dst.origin.z, copySize.depth)) { return DAWN_VALIDATION_ERROR( "Copy subresources cannot be overlapped when copying within the same " "texture."); } } return {}; } MaybeError ValidateCanUseAs(const TextureBase* texture, wgpu::TextureUsage usage) { ASSERT(wgpu::HasZeroOrOneBits(usage)); if (!(texture->GetUsage() & usage)) { return DAWN_VALIDATION_ERROR("texture doesn't have the required usage."); } return {}; } MaybeError ValidateCanUseAs(const BufferBase* buffer, wgpu::BufferUsage usage) { ASSERT(wgpu::HasZeroOrOneBits(usage)); if (!(buffer->GetUsage() & usage)) { return DAWN_VALIDATION_ERROR("buffer doesn't have the required usage."); } return {}; } } // namespace dawn_native