// // Copyright 2019 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // mtl_command_buffer.mm: // Implementations of Metal framework's MTLCommandBuffer, MTLCommandQueue, // MTLCommandEncoder's wrappers. // #ifdef UNSAFE_BUFFERS_BUILD # pragma allow_unsafe_libc_calls #endif #include "libANGLE/renderer/metal/mtl_command_buffer.h" #include #include #include #include #include "mtl_command_buffer.h" #if ANGLE_MTL_SIMULATE_DISCARD_FRAMEBUFFER # include #endif #include "common/debug.h" #include "libANGLE/renderer/metal/mtl_occlusion_query_pool.h" #include "libANGLE/renderer/metal/mtl_resources.h" #include "libANGLE/renderer/metal/mtl_utils.h" // Use to compare the new values with the values already set in the command encoder: static inline bool operator==(const MTLViewport &lhs, const MTLViewport &rhs) { return memcmp(&lhs, &rhs, sizeof(lhs)) == 0; } static inline bool operator==(const MTLScissorRect &lhs, const MTLScissorRect &rhs) { return memcmp(&lhs, &rhs, sizeof(lhs)) == 0; } namespace rx { namespace mtl { namespace { #define ANGLE_MTL_CMD_X(PROC) \ PROC(Invalid) \ PROC(SetRenderPipelineState) \ PROC(SetTriangleFillMode) \ PROC(SetFrontFacingWinding) \ PROC(SetCullMode) \ PROC(SetDepthStencilState) \ PROC(SetDepthBias) \ PROC(SetDepthClipMode) \ PROC(SetStencilRefVals) \ PROC(SetViewport) \ PROC(SetScissorRect) \ PROC(SetBlendColor) \ PROC(SetVertexBuffer) \ PROC(SetVertexBufferOffset) \ PROC(SetVertexBytes) \ PROC(SetVertexSamplerState) \ PROC(SetVertexTexture) \ PROC(SetFragmentBuffer) \ PROC(SetFragmentBufferOffset) \ PROC(SetFragmentBytes) \ PROC(SetFragmentSamplerState) \ PROC(SetFragmentTexture) \ PROC(Draw) \ PROC(DrawInstanced) \ PROC(DrawInstancedBaseInstance) \ PROC(DrawIndexed) \ PROC(DrawIndexedInstanced) \ PROC(DrawIndexedInstancedBaseVertexBaseInstance) \ PROC(SetVisibilityResultMode) \ PROC(UseResource) \ PROC(MemoryBarrier) \ PROC(MemoryBarrierWithResource) \ PROC(InsertDebugSignpost) \ PROC(PushDebugGroup) \ PROC(PopDebugGroup) #define ANGLE_MTL_TYPE_DECL(CMD) CMD, // Command types enum class CmdType : uint8_t { ANGLE_MTL_CMD_X(ANGLE_MTL_TYPE_DECL) }; // Commands decoder inline void InvalidCmd(id encoder, IntermediateCommandStream *stream) { UNREACHABLE(); } inline void SetRenderPipelineStateCmd(id encoder, IntermediateCommandStream *stream) { id state = stream->fetch>(); [encoder setRenderPipelineState:state]; [state ANGLE_MTL_RELEASE]; } inline void SetTriangleFillModeCmd(id encoder, IntermediateCommandStream *stream) { MTLTriangleFillMode mode = stream->fetch(); [encoder setTriangleFillMode:mode]; } inline void SetFrontFacingWindingCmd(id encoder, IntermediateCommandStream *stream) { MTLWinding winding = stream->fetch(); [encoder setFrontFacingWinding:winding]; } inline void SetCullModeCmd(id encoder, IntermediateCommandStream *stream) { MTLCullMode mode = stream->fetch(); [encoder setCullMode:mode]; } inline void SetDepthStencilStateCmd(id encoder, IntermediateCommandStream *stream) { id state = stream->fetch>(); [encoder setDepthStencilState:state]; [state ANGLE_MTL_RELEASE]; } inline void SetDepthBiasCmd(id encoder, IntermediateCommandStream *stream) { float depthBias = stream->fetch(); float slopeScale = stream->fetch(); float clamp = stream->fetch(); [encoder setDepthBias:depthBias slopeScale:slopeScale clamp:clamp]; } inline void SetDepthClipModeCmd(id encoder, IntermediateCommandStream *stream) { MTLDepthClipMode depthClipMode = stream->fetch(); [encoder setDepthClipMode:depthClipMode]; } inline void SetStencilRefValsCmd(id encoder, IntermediateCommandStream *stream) { // Metal has some bugs when reference values are larger than 0xff uint32_t frontRef = stream->fetch(); uint32_t backRef = stream->fetch(); [encoder setStencilFrontReferenceValue:frontRef backReferenceValue:backRef]; } inline void SetViewportCmd(id encoder, IntermediateCommandStream *stream) { MTLViewport viewport = stream->fetch(); [encoder setViewport:viewport]; } inline void SetScissorRectCmd(id encoder, IntermediateCommandStream *stream) { MTLScissorRect rect = stream->fetch(); [encoder setScissorRect:rect]; } inline void SetBlendColorCmd(id encoder, IntermediateCommandStream *stream) { float r = stream->fetch(); float g = stream->fetch(); float b = stream->fetch(); float a = stream->fetch(); [encoder setBlendColorRed:r green:g blue:b alpha:a]; } inline void SetVertexBufferCmd(id encoder, IntermediateCommandStream *stream) { id buffer = stream->fetch>(); uint32_t offset = stream->fetch(); uint32_t index = stream->fetch(); [encoder setVertexBuffer:buffer offset:offset atIndex:index]; [buffer ANGLE_MTL_RELEASE]; } inline void SetVertexBufferOffsetCmd(id encoder, IntermediateCommandStream *stream) { uint32_t offset = stream->fetch(); uint32_t index = stream->fetch(); [encoder setVertexBufferOffset:offset atIndex:index]; } inline void SetVertexBytesCmd(id encoder, IntermediateCommandStream *stream) { size_t size = stream->fetch(); const uint8_t *bytes = stream->fetch(size); uint32_t index = stream->fetch(); [encoder setVertexBytes:bytes length:size atIndex:index]; } inline void SetVertexSamplerStateCmd(id encoder, IntermediateCommandStream *stream) { id state = stream->fetch>(); float lodMinClamp = stream->fetch(); float lodMaxClamp = stream->fetch(); uint32_t index = stream->fetch(); [encoder setVertexSamplerState:state lodMinClamp:lodMinClamp lodMaxClamp:lodMaxClamp atIndex:index]; [state ANGLE_MTL_RELEASE]; } inline void SetVertexTextureCmd(id encoder, IntermediateCommandStream *stream) { id texture = stream->fetch>(); uint32_t index = stream->fetch(); [encoder setVertexTexture:texture atIndex:index]; [texture ANGLE_MTL_RELEASE]; } inline void SetFragmentBufferCmd(id encoder, IntermediateCommandStream *stream) { id buffer = stream->fetch>(); uint32_t offset = stream->fetch(); uint32_t index = stream->fetch(); [encoder setFragmentBuffer:buffer offset:offset atIndex:index]; [buffer ANGLE_MTL_RELEASE]; } inline void SetFragmentBufferOffsetCmd(id encoder, IntermediateCommandStream *stream) { uint32_t offset = stream->fetch(); uint32_t index = stream->fetch(); [encoder setFragmentBufferOffset:offset atIndex:index]; } inline void SetFragmentBytesCmd(id encoder, IntermediateCommandStream *stream) { size_t size = stream->fetch(); const uint8_t *bytes = stream->fetch(size); uint32_t index = stream->fetch(); [encoder setFragmentBytes:bytes length:size atIndex:index]; } inline void SetFragmentSamplerStateCmd(id encoder, IntermediateCommandStream *stream) { id state = stream->fetch>(); float lodMinClamp = stream->fetch(); float lodMaxClamp = stream->fetch(); uint32_t index = stream->fetch(); [encoder setFragmentSamplerState:state lodMinClamp:lodMinClamp lodMaxClamp:lodMaxClamp atIndex:index]; [state ANGLE_MTL_RELEASE]; } inline void SetFragmentTextureCmd(id encoder, IntermediateCommandStream *stream) { id texture = stream->fetch>(); uint32_t index = stream->fetch(); [encoder setFragmentTexture:texture atIndex:index]; [texture ANGLE_MTL_RELEASE]; } inline void DrawCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t vertexStart = stream->fetch(); uint32_t vertexCount = stream->fetch(); [encoder drawPrimitives:primitiveType vertexStart:vertexStart vertexCount:vertexCount]; } inline void DrawInstancedCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t vertexStart = stream->fetch(); uint32_t vertexCount = stream->fetch(); uint32_t instances = stream->fetch(); [encoder drawPrimitives:primitiveType vertexStart:vertexStart vertexCount:vertexCount instanceCount:instances]; } inline void DrawInstancedBaseInstanceCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t vertexStart = stream->fetch(); uint32_t vertexCount = stream->fetch(); uint32_t instances = stream->fetch(); uint32_t baseInstance = stream->fetch(); [encoder drawPrimitives:primitiveType vertexStart:vertexStart vertexCount:vertexCount instanceCount:instances baseInstance:baseInstance]; } inline void DrawIndexedCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t indexCount = stream->fetch(); MTLIndexType indexType = stream->fetch(); id indexBuffer = stream->fetch>(); size_t bufferOffset = stream->fetch(); [encoder drawIndexedPrimitives:primitiveType indexCount:indexCount indexType:indexType indexBuffer:indexBuffer indexBufferOffset:bufferOffset]; [indexBuffer ANGLE_MTL_RELEASE]; } inline void DrawIndexedInstancedCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t indexCount = stream->fetch(); MTLIndexType indexType = stream->fetch(); id indexBuffer = stream->fetch>(); size_t bufferOffset = stream->fetch(); uint32_t instances = stream->fetch(); [encoder drawIndexedPrimitives:primitiveType indexCount:indexCount indexType:indexType indexBuffer:indexBuffer indexBufferOffset:bufferOffset instanceCount:instances]; [indexBuffer ANGLE_MTL_RELEASE]; } inline void DrawIndexedInstancedBaseVertexBaseInstanceCmd(id encoder, IntermediateCommandStream *stream) { MTLPrimitiveType primitiveType = stream->fetch(); uint32_t indexCount = stream->fetch(); MTLIndexType indexType = stream->fetch(); id indexBuffer = stream->fetch>(); size_t bufferOffset = stream->fetch(); uint32_t instances = stream->fetch(); uint32_t baseVertex = stream->fetch(); uint32_t baseInstance = stream->fetch(); [encoder drawIndexedPrimitives:primitiveType indexCount:indexCount indexType:indexType indexBuffer:indexBuffer indexBufferOffset:bufferOffset instanceCount:instances baseVertex:baseVertex baseInstance:baseInstance]; [indexBuffer ANGLE_MTL_RELEASE]; } inline void SetVisibilityResultModeCmd(id encoder, IntermediateCommandStream *stream) { MTLVisibilityResultMode mode = stream->fetch(); size_t offset = stream->fetch(); [encoder setVisibilityResultMode:mode offset:offset]; } inline void UseResourceCmd(id encoder, IntermediateCommandStream *stream) { id resource = stream->fetch>(); MTLResourceUsage usage = stream->fetch(); MTLRenderStages stages = stream->fetch(); [encoder useResource:resource usage:usage stages:stages]; [resource ANGLE_MTL_RELEASE]; } inline void MemoryBarrierCmd(id encoder, IntermediateCommandStream *stream) { MTLBarrierScope scope = stream->fetch(); MTLRenderStages after = stream->fetch(); MTLRenderStages before = stream->fetch(); [encoder memoryBarrierWithScope:scope afterStages:after beforeStages:before]; } inline void MemoryBarrierWithResourceCmd(id encoder, IntermediateCommandStream *stream) { id resource = stream->fetch>(); MTLRenderStages after = stream->fetch(); MTLRenderStages before = stream->fetch(); [encoder memoryBarrierWithResources:&resource count:1 afterStages:after beforeStages:before]; [resource ANGLE_MTL_RELEASE]; } inline void InsertDebugSignpostCmd(id encoder, IntermediateCommandStream *stream) { NSString *label = stream->fetch(); [encoder insertDebugSignpost:label]; [label ANGLE_MTL_RELEASE]; } inline void PushDebugGroupCmd(id encoder, IntermediateCommandStream *stream) { NSString *label = stream->fetch(); [encoder pushDebugGroup:label]; [label ANGLE_MTL_RELEASE]; } inline void PopDebugGroupCmd(id encoder, IntermediateCommandStream *stream) { [encoder popDebugGroup]; } NSString *cppLabelToObjC(const std::string &marker) { NSString *label = [NSString stringWithUTF8String:marker.c_str()]; if (!label) { // This can happen if the string is not a valid ascii string. label = @"Invalid ASCII string"; } return label; } inline void CheckPrimitiveType(MTLPrimitiveType primitiveType) { if (ANGLE_UNLIKELY(primitiveType == MTLPrimitiveTypeInvalid)) { // Should have been caught by validation higher up. FATAL() << "invalid primitive type was uncaught by validation"; } } template void RandomizeClearValue(ObjCAttachmentDescriptor *objCRenderPassAttachment) { std::random_device rd; if constexpr (std::is_same_v) { std::uniform_real_distribution dist(0.f, 1.f); objCRenderPassAttachment.clearColor = MTLClearColorMake(dist(rd), dist(rd), dist(rd), dist(rd)); } else if constexpr (std::is_same_v) { std::uniform_real_distribution dist(0.f, 1.f); objCRenderPassAttachment.clearDepth = dist(rd); } else if constexpr (std::is_same_v) { std::uniform_int_distribution dist(0, 255); objCRenderPassAttachment.clearStencil = dist(rd); } } } // namespace // AtomicSerial implementation void AtomicSerial::storeMaxValue(uint64_t value) { uint64_t prevValue = load(); while (prevValue < value && !mValue.compare_exchange_weak(prevValue, value, std::memory_order_release, std::memory_order_consume)) { } } // CommandQueue implementation void CommandQueue::reset() { finishAllCommands(); ParentClass::reset(); } void CommandQueue::set(id metalQueue) { finishAllCommands(); ParentClass::set(metalQueue); } void CommandQueue::finishAllCommands() { std::deque commandBuffers; { std::lock_guard lg(mLock); mMetalCmdBuffers.swap(commandBuffers); } for (CmdBufferQueueEntry &entry : commandBuffers) { [entry.buffer waitUntilCompleted]; } } void CommandQueue::ensureResourceReadyForCPU(const ResourceRef &resource) { if (!resource) { return; } ensureResourceReadyForCPU(resource.get()); } void CommandQueue::ensureResourceReadyForCPU(Resource *resource) { mLock.lock(); while (isResourceBeingUsedByGPU(resource) && !mMetalCmdBuffers.empty()) { CmdBufferQueueEntry metalBufferEntry = mMetalCmdBuffers.front(); mMetalCmdBuffers.pop_front(); mLock.unlock(); ANGLE_MTL_LOG("Waiting for MTLCommandBuffer %llu:%p", metalBufferEntry.serial, metalBufferEntry.buffer.get()); [metalBufferEntry.buffer waitUntilCompleted]; mLock.lock(); } mLock.unlock(); // This can happen if the resource is read then write in the same command buffer. // So it is the responsitibily of outer code to ensure the command buffer is commit before // the resource can be read or written again ASSERT(!isResourceBeingUsedByGPU(resource)); } bool CommandQueue::isResourceBeingUsedByGPU(const Resource *resource) const { if (!resource) { return false; } return !isSerialCompleted(resource->getCommandBufferQueueSerial()); } bool CommandQueue::resourceHasPendingWorks(const Resource *resource) const { if (!resource) { return false; } return mCommittedBufferSerial.load() < resource->getCommandBufferQueueSerial(); } bool CommandQueue::resourceHasPendingRenderWorks(const Resource *resource) const { if (!resource) { return false; } return mCommittedBufferSerial.load() < resource->getLastRenderEncoderSerial(); } bool CommandQueue::isSerialCompleted(uint64_t serial) const { return mCompletedBufferSerial.load() >= serial; } bool CommandQueue::waitUntilSerialCompleted(uint64_t serial, uint64_t timeoutNs) const { std::unique_lock lk(mLock); if (isSerialCompleted(serial)) { return true; } if (timeoutNs == 0 || !mCompletedBufferSerialCv.wait_for(lk, std::chrono::nanoseconds(timeoutNs), [&]() { return isSerialCompleted(serial); })) { return false; } return true; } bool CommandQueue::isSerialScheduled(uint64_t serial) const { return mScheduledBufferSerial.load() >= serial; } void CommandQueue::addCommandBufferScheduledCallback(uint64_t serial, std::function callback) { std::lock_guard lg(mLock); if (isSerialScheduled(serial)) { // Run the callback immediately if the command buffer for `serial` was already scheduled. callback(); } else { mCommandBufferScheduledCallbacks[serial].push_back(std::move(callback)); } } angle::ObjCPtr> CommandQueue::makeMetalCommandBuffer(uint64_t *queueSerialOut) { ANGLE_MTL_OBJC_SCOPE { angle::ObjCPtr> metalCmdBuffer = [get() commandBuffer]; std::lock_guard lg(mLock); uint64_t serial = mQueueSerialCounter++; uint64_t timeElapsedEntry = mActiveTimeElapsedId; mMetalCmdBuffers.push_back({metalCmdBuffer, serial}); ANGLE_MTL_LOG("Created MTLCommandBuffer %llu:%p", serial, metalCmdBuffer.get()); if (timeElapsedEntry) { addCommandBufferToTimeElapsedEntry(lg, timeElapsedEntry); } [metalCmdBuffer addScheduledHandler:^(id buf) { onCommandBufferScheduled(serial); }]; [metalCmdBuffer addCompletedHandler:^(id buf) { onCommandBufferCompleted(buf, serial, timeElapsedEntry); }]; ASSERT(metalCmdBuffer); *queueSerialOut = serial; return metalCmdBuffer; } } void CommandQueue::onCommandBufferCommitted(id buf, uint64_t serial) { ANGLE_MTL_LOG("Committed MTLCommandBuffer %llu:%p", serial, buf); mCommittedBufferSerial.storeMaxValue(serial); } void CommandQueue::onCommandBufferScheduled(uint64_t serial) { std::vector> callbacks; { std::lock_guard lg(mLock); auto it = mCommandBufferScheduledCallbacks.find(serial); if (it != mCommandBufferScheduledCallbacks.end()) { callbacks = std::move(it->second); mCommandBufferScheduledCallbacks.erase(it); } mScheduledBufferSerial.storeMaxValue(serial); } for (const auto &callback : callbacks) { callback(); } } void CommandQueue::onCommandBufferCompleted(id buf, uint64_t serial, uint64_t timeElapsedEntry) { std::lock_guard lg(mLock); ANGLE_MTL_LOG("Completed MTLCommandBuffer %llu:%p", serial, buf); NSError *error = buf.error; if (error) { ERR() << "Completed MTLCommandBuffer failed, and error is " << error.localizedDescription.UTF8String; mCmdBufferError.store(static_cast(error.code)); } if (timeElapsedEntry != 0) { // Record this command buffer's elapsed time. recordCommandBufferTimeElapsed(lg, timeElapsedEntry, [buf GPUEndTime] - [buf GPUStartTime]); } if (mCompletedBufferSerial.load() >= serial) { // Already handled. return; } while (!mMetalCmdBuffers.empty() && mMetalCmdBuffers.front().serial <= serial) { CmdBufferQueueEntry metalBufferEntry = mMetalCmdBuffers.front(); ANGLE_UNUSED_VARIABLE(metalBufferEntry); ANGLE_MTL_LOG("Popped MTLCommandBuffer %llu:%p", metalBufferEntry.serial, metalBufferEntry.buffer.get()); mMetalCmdBuffers.pop_front(); } mCompletedBufferSerial.storeMaxValue(serial); mCompletedBufferSerialCv.notify_all(); } uint64_t CommandQueue::getNextRenderPassEncoderSerial() { return ++mRenderEncoderCounter; } uint64_t CommandQueue::allocateTimeElapsedEntry() { std::lock_guard lg(mLock); uint64_t id = mTimeElapsedNextId++; if (mTimeElapsedNextId == 0) { mTimeElapsedNextId = 1; } TimeElapsedEntry entry; entry.id = id; mTimeElapsedEntries.insert({id, entry}); return id; } bool CommandQueue::deleteTimeElapsedEntry(uint64_t id) { std::lock_guard lg(mLock); auto result = mTimeElapsedEntries.find(id); if (result == mTimeElapsedEntries.end()) { return false; } mTimeElapsedEntries.erase(result); return true; } void CommandQueue::setActiveTimeElapsedEntry(uint64_t id) { std::lock_guard lg(mLock); // If multithreading support is added to the Metal backend and // involves accessing the same CommandQueue from multiple threads, // the time elapsed query implementation will need to be rethought. mActiveTimeElapsedId = id; } bool CommandQueue::isTimeElapsedEntryComplete(uint64_t id) { std::lock_guard lg(mLock); auto result = mTimeElapsedEntries.find(id); if (result == mTimeElapsedEntries.end()) { return false; } TimeElapsedEntry &entry = result->second; ASSERT(entry.pending_command_buffers >= 0); if (entry.pending_command_buffers > 0) { return false; } return true; } double CommandQueue::getTimeElapsedEntryInSeconds(uint64_t id) { std::lock_guard lg(mLock); auto result = mTimeElapsedEntries.find(id); if (result == mTimeElapsedEntries.end()) { return 0.0; } return result->second.elapsed_seconds; } // Private. void CommandQueue::addCommandBufferToTimeElapsedEntry(std::lock_guard &lg, uint64_t id) { // This must be called under the cover of mLock. auto result = mTimeElapsedEntries.find(id); if (result == mTimeElapsedEntries.end()) { return; } TimeElapsedEntry &entry = result->second; ++entry.pending_command_buffers; } void CommandQueue::recordCommandBufferTimeElapsed(std::lock_guard &lg, uint64_t id, double seconds) { // This must be called under the cover of mLock. auto result = mTimeElapsedEntries.find(id); if (result == mTimeElapsedEntries.end()) { return; } TimeElapsedEntry &entry = result->second; ASSERT(entry.pending_command_buffers > 0); --entry.pending_command_buffers; entry.elapsed_seconds += seconds; } // CommandBuffer implementation CommandBuffer::CommandBuffer(CommandQueue *cmdQueue) : mCmdQueue(*cmdQueue) {} CommandBuffer::~CommandBuffer() { commit(WaitUntilFinished); cleanup(); } bool CommandBuffer::ready() const { std::lock_guard lg(mLock); return readyImpl(); } void CommandBuffer::commit(CommandBufferFinishOperation operation) { std::lock_guard lg(mLock); if (commitImpl()) { wait(operation); } } void CommandBuffer::wait(CommandBufferFinishOperation operation) { // NOTE: A CommandBuffer is valid forever under current conditions (2022-12-22) // except before the first call to restart. if (!valid()) { return; } // You can't wait on an uncommitted command buffer. ASSERT(mCommitted); switch (operation) { case NoWait: break; case WaitUntilScheduled: // Only wait if we haven't already waited if (!mCmdQueue.isSerialScheduled(mQueueSerial)) { [get() waitUntilScheduled]; } break; case WaitUntilFinished: // Only wait if we haven't already waited until finished. if (!mCmdQueue.isSerialCompleted(mQueueSerial)) { [get() waitUntilCompleted]; } break; } } void CommandBuffer::present(id presentationDrawable) { [get() presentDrawable:presentationDrawable]; } void CommandBuffer::setResourceUsedByCommandBuffer(const ResourceRef &resource) { if (resource) { auto result = mResourceList.insert(resource->getID()); // If we were able to add a unique Metal resource ID to the list, count it. // // Note that we store Metal IDs here, properly retained in non-ARC environments, rather than // the ResourceRefs. There are some assumptions in TextureMtl in particular about weak refs // to temporary textures being cleared out eagerly. Holding on to additional references here // implies that that texture is still being used, and would require additional code to clear // out temporary render targets upon texture redefinition. if (result.second) { [resource->getID() ANGLE_MTL_RETAIN]; mWorkingResourceSize += resource->estimatedByteSize(); } } } void CommandBuffer::clearResourceListAndSize() { for (const id &metalID : mResourceList) { [metalID ANGLE_MTL_RELEASE]; } mResourceList.clear(); mWorkingResourceSize = 0; } void CommandBuffer::setWriteDependency(const ResourceRef &resource, bool isRenderCommand) { if (!resource) { return; } std::lock_guard lg(mLock); if (!readyImpl()) { return; } resource->setUsedByCommandBufferWithQueueSerial(mQueueSerial, true, isRenderCommand); setResourceUsedByCommandBuffer(resource); } void CommandBuffer::setReadDependency(const ResourceRef &resource, bool isRenderCommand) { setReadDependency(resource.get(), isRenderCommand); setResourceUsedByCommandBuffer(resource); } void CommandBuffer::setReadDependency(Resource *resource, bool isRenderCommand) { if (!resource) { return; } std::lock_guard lg(mLock); if (!readyImpl()) { return; } resource->setUsedByCommandBufferWithQueueSerial(mQueueSerial, false, isRenderCommand); } bool CommandBuffer::needsFlushForDrawCallLimits() const { return mWorkingResourceSize > kMaximumResidentMemorySizeInBytes; } uint64_t CommandBuffer::getQueueSerial() const { std::lock_guard lg(mLock); return mQueueSerial; } void CommandBuffer::restart() { uint64_t serial = 0; angle::ObjCPtr> metalCmdBuffer = mCmdQueue.makeMetalCommandBuffer(&serial); std::lock_guard lg(mLock); set(metalCmdBuffer); mQueueSerial = serial; mCommitted = false; for (std::string &marker : mDebugGroups) { pushDebugGroupImpl(marker); } clearResourceListAndSize(); ASSERT(metalCmdBuffer); } void CommandBuffer::insertDebugSignpost(const std::string &marker) { mtl::CommandEncoder *currentEncoder = getPendingCommandEncoder(); if (currentEncoder) { ANGLE_MTL_OBJC_SCOPE { NSString *label = cppLabelToObjC(marker); currentEncoder->insertDebugSignpost(label); } } else { mPendingDebugSignposts.push_back(marker); } } void CommandBuffer::pushDebugGroup(const std::string &marker) { mDebugGroups.push_back(marker); std::lock_guard lg(mLock); if (readyImpl()) { pushDebugGroupImpl(marker); } } void CommandBuffer::popDebugGroup() { if (!mDebugGroups.empty()) { mDebugGroups.pop_back(); } std::lock_guard lg(mLock); if (readyImpl()) { return; } } uint64_t CommandBuffer::queueEventSignal(id event, uint64_t value) { std::lock_guard lg(mLock); ASSERT(readyImpl()); CommandEncoder *currentEncoder = getPendingCommandEncoder(); if (currentEncoder) { // We cannot set event when there is an active render pass, defer the setting until the pass // end. PendingEvent pending; pending.event = std::move(event); pending.signalValue = value; mPendingSignalEvents.push_back(std::move(pending)); } else { setEventImpl(event, value); } return mQueueSerial; } void CommandBuffer::serverWaitEvent(id event, uint64_t value) { std::lock_guard lg(mLock); ASSERT(readyImpl()); ASSERT(!getPendingCommandEncoder()); setPendingEvents(); [get() encodeWaitForEvent:event value:value]; } /** private use only */ void CommandBuffer::set(id metalBuffer) { ParentClass::set(metalBuffer); } void CommandBuffer::setActiveCommandEncoder(CommandEncoder *encoder) { if (encoder->getType() == CommandEncoder::Type::RENDER) { mActiveRenderEncoder = encoder; } else { mActiveBlitOrComputeEncoder = encoder; } for (std::string &marker : mPendingDebugSignposts) { ANGLE_MTL_OBJC_SCOPE { NSString *label = cppLabelToObjC(marker); encoder->insertDebugSignpost(label); } } mPendingDebugSignposts.clear(); } void CommandBuffer::invalidateActiveCommandEncoder(CommandEncoder *encoder) { if (mActiveRenderEncoder == encoder) { mActiveRenderEncoder = nullptr; } else if (mActiveBlitOrComputeEncoder == encoder) { mActiveBlitOrComputeEncoder = nullptr; } if (getPendingCommandEncoder() == nullptr) { // No active command encoder, we can safely encode event signalling now. setPendingEvents(); } } CommandEncoder *CommandBuffer::getPendingCommandEncoder() { // blit/compute encoder takes precedence over render encoder because the former is immediate // encoder i.e its native MTLCommandEncoder is already created. return mActiveBlitOrComputeEncoder ? mActiveBlitOrComputeEncoder : mActiveRenderEncoder; } void CommandBuffer::cleanup() { mActiveBlitOrComputeEncoder = mActiveRenderEncoder = nullptr; ParentClass::set(nil); } bool CommandBuffer::readyImpl() const { if (!ParentClass::valid()) { return false; } return !mCommitted; } bool CommandBuffer::commitImpl() { if (!readyImpl()) { return false; } // End the current encoders forceEndingAllEncoders(); // Encoding any pending event's signalling. setPendingEvents(); // Notify command queue mCmdQueue.onCommandBufferCommitted(get(), mQueueSerial); // Do the actual commit [get() enqueue]; [get() commit]; // Reset the working resource set. clearResourceListAndSize(); mCommitted = true; return true; } void CommandBuffer::forceEndingAllEncoders() { // End active blit/compute encoder first since it's immediate encoder. if (mActiveBlitOrComputeEncoder) { mActiveBlitOrComputeEncoder->endEncoding(); mActiveBlitOrComputeEncoder = nullptr; } // End render encoder last. This is possible because it is deferred encoder. if (mActiveRenderEncoder) { mActiveRenderEncoder->endEncoding(); mActiveRenderEncoder = nullptr; } } void CommandBuffer::setPendingEvents() { for (const PendingEvent &eventEntry : mPendingSignalEvents) { setEventImpl(eventEntry.event, eventEntry.signalValue); } mPendingSignalEvents.clear(); } void CommandBuffer::setEventImpl(id event, uint64_t value) { ASSERT(!getPendingCommandEncoder()); [get() encodeSignalEvent:event value:value]; } void CommandBuffer::pushDebugGroupImpl(const std::string &marker) { ANGLE_MTL_OBJC_SCOPE { NSString *label = cppLabelToObjC(marker); [get() pushDebugGroup:label]; CommandEncoder *currentEncoder = getPendingCommandEncoder(); if (currentEncoder) { currentEncoder->pushDebugGroup(label); } } } void CommandBuffer::popDebugGroupImpl() { CommandEncoder *currentEncoder = getPendingCommandEncoder(); if (currentEncoder) { currentEncoder->popDebugGroup(); } [get() popDebugGroup]; } // CommandEncoder implementation CommandEncoder::CommandEncoder(CommandBuffer *cmdBuffer, Type type) : mType(type), mCmdBuffer(*cmdBuffer) {} CommandEncoder::~CommandEncoder() { reset(); } void CommandEncoder::endEncoding() { [get() endEncoding]; reset(); } void CommandEncoder::reset() { ParentClass::reset(); mCmdBuffer.invalidateActiveCommandEncoder(this); } void CommandEncoder::set(id metalCmdEncoder) { ParentClass::set(metalCmdEncoder); // Set this as active encoder cmdBuffer().setActiveCommandEncoder(this); } CommandEncoder &CommandEncoder::markResourceBeingWrittenByGPU(const BufferRef &buffer) { cmdBuffer().setWriteDependency(buffer, isRenderEncoder()); return *this; } CommandEncoder &CommandEncoder::markResourceBeingWrittenByGPU(const TextureRef &texture) { cmdBuffer().setWriteDependency(texture, isRenderEncoder()); return *this; } void CommandEncoder::pushDebugGroup(NSString *label) { // Default implementation [get() pushDebugGroup:label]; } void CommandEncoder::popDebugGroup() { // Default implementation [get() popDebugGroup]; } void CommandEncoder::insertDebugSignpost(NSString *label) { insertDebugSignpostImpl(label); } void CommandEncoder::insertDebugSignpostImpl(NSString *label) { // Default implementation [get() insertDebugSignpost:label]; } // RenderCommandEncoderShaderStates implementation RenderCommandEncoderShaderStates::RenderCommandEncoderShaderStates() { reset(); } void RenderCommandEncoderShaderStates::reset() { for (id &buffer : buffers) { buffer = nil; } for (uint32_t &offset : bufferOffsets) { offset = 0; } for (id &sampler : samplers) { sampler = nil; } for (Optional> &lodClampRange : samplerLodClamps) { lodClampRange.reset(); } for (id &texture : textures) { texture = nil; } } // RenderCommandEncoderStates implementation RenderCommandEncoderStates::RenderCommandEncoderStates() { reset(); } void RenderCommandEncoderStates::reset() { renderPipeline = nil; triangleFillMode = MTLTriangleFillModeFill; winding = MTLWindingClockwise; cullMode = MTLCullModeNone; depthStencilState = nil; depthBias = depthSlopeScale = depthClamp = 0; depthClipMode = MTLDepthClipModeClip; stencilFrontRef = stencilBackRef = 0; viewport.reset(); scissorRect.reset(); blendColor = {0, 0, 0, 0}; for (RenderCommandEncoderShaderStates &shaderStates : perShaderStates) { shaderStates.reset(); } visibilityResultMode = MTLVisibilityResultModeDisabled; visibilityResultBufferOffset = 0; } // RenderCommandEncoder implemtation RenderCommandEncoder::RenderCommandEncoder(CommandBuffer *cmdBuffer, const OcclusionQueryPool &queryPool, bool emulateDontCareLoadOpWithRandomClear) : CommandEncoder(cmdBuffer, RENDER), mOcclusionQueryPool(queryPool), mEmulateDontCareLoadOpWithRandomClear(emulateDontCareLoadOpWithRandomClear) { ANGLE_MTL_OBJC_SCOPE { mCachedRenderPassDescObjC = [MTLRenderPassDescriptor renderPassDescriptor]; } static_assert(sizeof(uint8_t) == sizeof(CmdType), "CmdType was expected to be 8 bit"); for (gl::ShaderType shaderType : gl::AllShaderTypes()) { mSetBufferCmds[shaderType] = static_cast(CmdType::Invalid); mSetBytesCmds[shaderType] = static_cast(CmdType::Invalid); mSetTextureCmds[shaderType] = static_cast(CmdType::Invalid); mSetSamplerCmds[shaderType] = static_cast(CmdType::Invalid); } mSetBufferCmds[gl::ShaderType::Vertex] = static_cast(CmdType::SetVertexBuffer); mSetBufferCmds[gl::ShaderType::Fragment] = static_cast(CmdType::SetFragmentBuffer); mSetBufferOffsetCmds[gl::ShaderType::Vertex] = static_cast(CmdType::SetVertexBufferOffset); mSetBufferOffsetCmds[gl::ShaderType::Fragment] = static_cast(CmdType::SetFragmentBufferOffset); mSetBytesCmds[gl::ShaderType::Vertex] = static_cast(CmdType::SetVertexBytes); mSetBytesCmds[gl::ShaderType::Fragment] = static_cast(CmdType::SetFragmentBytes); mSetTextureCmds[gl::ShaderType::Vertex] = static_cast(CmdType::SetVertexTexture); mSetTextureCmds[gl::ShaderType::Fragment] = static_cast(CmdType::SetFragmentTexture); mSetSamplerCmds[gl::ShaderType::Vertex] = static_cast(CmdType::SetVertexSamplerState); mSetSamplerCmds[gl::ShaderType::Fragment] = static_cast(CmdType::SetFragmentSamplerState); } RenderCommandEncoder::~RenderCommandEncoder() {} void RenderCommandEncoder::reset() { CommandEncoder::reset(); mRecording = false; mPipelineStateSet = false; mCommands.clear(); } template bool RenderCommandEncoder::finalizeLoadStoreAction( const RenderPassAttachmentDesc &cppRenderPassAttachment, ObjCAttachmentDescriptor *objCRenderPassAttachment) { if (!objCRenderPassAttachment.texture) { objCRenderPassAttachment.loadAction = MTLLoadActionDontCare; objCRenderPassAttachment.storeAction = MTLStoreActionDontCare; objCRenderPassAttachment.resolveTexture = nil; return false; } if (objCRenderPassAttachment.resolveTexture) { if (objCRenderPassAttachment.storeAction == MTLStoreActionStore) { // NOTE(hqle): Currently if the store action with implicit MS texture is // MTLStoreAction, it is automatically converted to store and resolve action. It might // introduce unnecessary overhead. Consider an improvement such as only store the MS // texture, and resolve only at the end of real render pass (not render pass that was // interrupted by compute pass) or before glBlitFramebuffer operation starts. objCRenderPassAttachment.storeAction = MTLStoreActionStoreAndMultisampleResolve; } else if (objCRenderPassAttachment.storeAction == MTLStoreActionDontCare) { // Ignore resolve texture if the store action is not a resolve action. objCRenderPassAttachment.resolveTexture = nil; } } // Check if we need to disable MTLLoadActionLoad & MTLStoreActionStore mtl::TextureRef mainTextureRef = cppRenderPassAttachment.getImplicitMSTextureIfAvailOrTexture(); ASSERT(mainTextureRef->get() == objCRenderPassAttachment.texture); if (mainTextureRef->shouldNotLoadStore()) { // Disable Load. if (objCRenderPassAttachment.loadAction == MTLLoadActionLoad) { objCRenderPassAttachment.loadAction = MTLLoadActionDontCare; } // Disable Store. if (objCRenderPassAttachment.storeAction == MTLStoreActionStore) { objCRenderPassAttachment.storeAction = MTLStoreActionDontCare; } else if (objCRenderPassAttachment.storeAction == MTLStoreActionStoreAndMultisampleResolve) { objCRenderPassAttachment.storeAction = MTLStoreActionMultisampleResolve; } } // If the texture has emulated format such as RGB, we need to clear the texture so that the // alpha channel will always be one. Otherwise DontCare loadAction would have set the alpha // channel to garbage values. if constexpr (std::is_same_v) { if (objCRenderPassAttachment.loadAction == MTLLoadActionDontCare && mainTextureRef->getColorWritableMask() != MTLColorWriteMaskAll) { objCRenderPassAttachment.loadAction = MTLLoadActionClear; objCRenderPassAttachment.clearColor = MTLClearColorMake(0, 0, 0, kEmulatedAlphaValue); } } if (ANGLE_UNLIKELY(mEmulateDontCareLoadOpWithRandomClear && objCRenderPassAttachment.loadAction == MTLLoadActionDontCare)) { // Emulate DontCare loadAction with Clear. This is useful for testing undefined values // caused by DontCare loadAction on non-tiled GPUs. objCRenderPassAttachment.loadAction = MTLLoadActionClear; RandomizeClearValue(objCRenderPassAttachment); } if (objCRenderPassAttachment.storeAction == MTLStoreActionUnknown) { // If storeAction hasn't been set for this attachment, we set to dontcare. objCRenderPassAttachment.storeAction = MTLStoreActionDontCare; } return true; } void RenderCommandEncoder::endEncoding() { endEncodingImpl(true); } void RenderCommandEncoder::endEncodingImpl(bool considerDiscardSimulation) { if (!valid()) return; bool hasAttachment = false; // Last minute correcting the store options. MTLRenderPassDescriptor *objCRenderPassDesc = mCachedRenderPassDescObjC.get(); for (uint32_t i = 0; i < mRenderPassDesc.numColorAttachments; ++i) { // Update store action set between restart() and endEncoding() objCRenderPassDesc.colorAttachments[i].storeAction = mRenderPassDesc.colorAttachments[i].storeAction; if (finalizeLoadStoreAction(mRenderPassDesc.colorAttachments[i], objCRenderPassDesc.colorAttachments[i])) hasAttachment = true; } // Update store action set between restart() and endEncoding() objCRenderPassDesc.depthAttachment.storeAction = mRenderPassDesc.depthAttachment.storeAction; if (finalizeLoadStoreAction(mRenderPassDesc.depthAttachment, objCRenderPassDesc.depthAttachment)) hasAttachment = true; // Update store action set between restart() and endEncoding() objCRenderPassDesc.stencilAttachment.storeAction = mRenderPassDesc.stencilAttachment.storeAction; if (finalizeLoadStoreAction(mRenderPassDesc.stencilAttachment, objCRenderPassDesc.stencilAttachment)) hasAttachment = true; // Set visibility result buffer if (mOcclusionQueryPool.getNumRenderPassAllocatedQueries()) { objCRenderPassDesc.visibilityResultBuffer = mOcclusionQueryPool.getRenderPassVisibilityPoolBuffer()->get(); } else { objCRenderPassDesc.visibilityResultBuffer = nil; } // If a render pass has intended side effects but no attachments, the app must set a default // width/height. bool hasSideEffects = hasAttachment || (mRenderPassDesc.defaultWidth != 0 && mRenderPassDesc.defaultHeight != 0); // Encode the actual encoder. It will not be created when there are no side effects. if (hasSideEffects) { // Metal validation messages say: Either set rendertargets in RenderPassDescriptor or set // defaultRasterSampleCount. ASSERT(hasAttachment || objCRenderPassDesc.defaultRasterSampleCount != 0); encodeMetalEncoder(); } else if (!hasSideEffects && hasDrawCalls()) { // Command encoder should not have been created if no side effects occur, but draw calls do. UNREACHABLE(); // Fallback to clearing commands if on release. mCommands.clear(); } // If no side effects, and no drawing is encoded, there's no point in encoding. Skip the // commands. else { mCommands.clear(); } CommandEncoder::endEncoding(); #if ANGLE_MTL_SIMULATE_DISCARD_FRAMEBUFFER if (considerDiscardSimulation) { simulateDiscardFramebuffer(); } #endif // reset state mRenderPassDesc = RenderPassDesc(); mStateCache.reset(); } inline void RenderCommandEncoder::initAttachmentWriteDependencyAndScissorRect( const RenderPassAttachmentDesc &attachment) { TextureRef texture = attachment.texture; if (texture) { cmdBuffer().setWriteDependency(texture, /*isRenderCommand=*/true); const MipmapNativeLevel &mipLevel = attachment.level; mRenderPassMaxScissorRect.width = std::min(mRenderPassMaxScissorRect.width, texture->width(mipLevel)); mRenderPassMaxScissorRect.height = std::min(mRenderPassMaxScissorRect.height, texture->height(mipLevel)); } } inline void RenderCommandEncoder::initWriteDependency(const TextureRef &texture) { if (texture) { cmdBuffer().setWriteDependency(texture, /*isRenderCommand=*/true); } } void RenderCommandEncoder::simulateDiscardFramebuffer() { // Simulate true framebuffer discard operation by clearing the framebuffer #if ANGLE_MTL_SIMULATE_DISCARD_FRAMEBUFFER std::random_device rd; // Will be used to obtain a seed for the random number engine std::mt19937 gen(rd()); // Standard mersenne_twister_engine seeded with rd() std::uniform_real_distribution dis(0.0, 1.0f); bool hasDiscard = false; for (uint32_t i = 0; i < mRenderPassDesc.numColorAttachments; ++i) { if (mRenderPassDesc.colorAttachments[i].storeAction == MTLStoreActionDontCare) { hasDiscard = true; mRenderPassDesc.colorAttachments[i].loadAction = MTLLoadActionClear; mRenderPassDesc.colorAttachments[i].clearColor = MTLClearColorMake(dis(gen), dis(gen), dis(gen), dis(gen)); } else { mRenderPassDesc.colorAttachments[i].loadAction = MTLLoadActionLoad; } } if (mRenderPassDesc.depthAttachment.storeAction == MTLStoreActionDontCare) { hasDiscard = true; mRenderPassDesc.depthAttachment.loadAction = MTLLoadActionClear; mRenderPassDesc.depthAttachment.clearDepth = dis(gen); } else { mRenderPassDesc.depthAttachment.loadAction = MTLLoadActionLoad; } if (mRenderPassDesc.stencilAttachment.storeAction == MTLStoreActionDontCare) { hasDiscard = true; mRenderPassDesc.stencilAttachment.loadAction = MTLLoadActionClear; mRenderPassDesc.stencilAttachment.clearStencil = rand(); } else { mRenderPassDesc.stencilAttachment.loadAction = MTLLoadActionLoad; } if (hasDiscard) { MTLRenderPassDescriptor tmpDesc = mRenderPassDesc; restart(tmpDesc); endEncodingImpl(false); } #endif // ANGLE_MTL_SIMULATE_DISCARD_FRAMEBUFFER } void RenderCommandEncoder::encodeMetalEncoder() { ANGLE_MTL_OBJC_SCOPE { ANGLE_MTL_LOG("Creating new render command encoder with desc: %@", [mCachedRenderPassDescObjC description]); id metalCmdEncoder = [cmdBuffer().get() renderCommandEncoderWithDescriptor:mCachedRenderPassDescObjC]; set(metalCmdEncoder); // Verify that it was created successfully ASSERT(get()); // Work-around driver bug on iOS devices: stencil must be explicitly set to zero // even if the doc says the default value is already zero. [metalCmdEncoder setStencilReferenceValue:0]; if (mLabel) { metalCmdEncoder.label = mLabel; } while (mCommands.good()) { CmdType cmdType = mCommands.fetch(); switch (cmdType) { #define ANGLE_MTL_CMD_MAP(CMD) \ case CmdType::CMD: \ CMD##Cmd(metalCmdEncoder, &mCommands); \ break; ANGLE_MTL_CMD_X(ANGLE_MTL_CMD_MAP) #undef ANGLE_MTL_CMD_MAP } } mCommands.clear(); } } RenderCommandEncoder &RenderCommandEncoder::restart(const RenderPassDesc &desc, uint32_t deviceMaxRenderTargets) { if (valid()) { if (mRenderPassDesc == desc) { // no change, skip return *this; } // finish current encoder endEncoding(); } if (!cmdBuffer().ready()) { reset(); return *this; } mSerial = cmdBuffer().cmdQueue().getNextRenderPassEncoderSerial(); mRenderPassDesc = desc; mRecording = true; mHasDrawCalls = false; mRenderPassMaxScissorRect = {.x = 0, .y = 0, .width = std::numeric_limits::max(), .height = std::numeric_limits::max()}; // mask writing dependency & set appropriate store options for (uint32_t i = 0; i < mRenderPassDesc.numColorAttachments; ++i) { initAttachmentWriteDependencyAndScissorRect(mRenderPassDesc.colorAttachments[i]); } initAttachmentWriteDependencyAndScissorRect(mRenderPassDesc.depthAttachment); initAttachmentWriteDependencyAndScissorRect(mRenderPassDesc.stencilAttachment); // Convert to Objective-C descriptor mRenderPassDesc.convertToMetalDesc(mCachedRenderPassDescObjC, deviceMaxRenderTargets); // The actual Objective-C encoder will be created later in endEncoding(), we do so in order // to be able to sort the commands or do the preprocessing before the actual encoding. // Since we defer the native encoder creation, we need to explicitly tell command buffer that // this object is the active encoder: cmdBuffer().setActiveCommandEncoder(this); return *this; } RenderCommandEncoder &RenderCommandEncoder::setRenderPipelineState(id state) { mPipelineStateSet = true; if (mStateCache.renderPipeline == state) { return *this; } mStateCache.renderPipeline = state; mCommands.push(CmdType::SetRenderPipelineState).push([state ANGLE_MTL_RETAIN]); return *this; } RenderCommandEncoder &RenderCommandEncoder::setTriangleFillMode(MTLTriangleFillMode mode) { if (mStateCache.triangleFillMode == mode) { return *this; } mStateCache.triangleFillMode = mode; mCommands.push(CmdType::SetTriangleFillMode).push(mode); return *this; } RenderCommandEncoder &RenderCommandEncoder::setFrontFacingWinding(MTLWinding winding) { if (mStateCache.winding == winding) { return *this; } mStateCache.winding = winding; mCommands.push(CmdType::SetFrontFacingWinding).push(winding); return *this; } RenderCommandEncoder &RenderCommandEncoder::setCullMode(MTLCullMode mode) { if (mStateCache.cullMode == mode) { return *this; } mStateCache.cullMode = mode; mCommands.push(CmdType::SetCullMode).push(mode); return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthStencilState(id state) { if (mStateCache.depthStencilState == state) { return *this; } mStateCache.depthStencilState = state; mCommands.push(CmdType::SetDepthStencilState).push([state ANGLE_MTL_RETAIN]); return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthBias(float depthBias, float slopeScale, float clamp) { if (mStateCache.depthBias == depthBias && mStateCache.depthSlopeScale == slopeScale && mStateCache.depthClamp == clamp) { return *this; } mStateCache.depthBias = depthBias; mStateCache.depthSlopeScale = slopeScale; mStateCache.depthClamp = clamp; mCommands.push(CmdType::SetDepthBias).push(depthBias).push(slopeScale).push(clamp); return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthClipMode(MTLDepthClipMode depthClipMode) { if (mStateCache.depthClipMode == depthClipMode) { return *this; } mStateCache.depthClipMode = depthClipMode; mCommands.push(CmdType::SetDepthClipMode).push(depthClipMode); return *this; } RenderCommandEncoder &RenderCommandEncoder::setStencilRefVals(uint32_t frontRef, uint32_t backRef) { // Metal has some bugs when reference values are larger than 0xff ASSERT(frontRef == (frontRef & kStencilMaskAll)); ASSERT(backRef == (backRef & kStencilMaskAll)); if (mStateCache.stencilFrontRef == frontRef && mStateCache.stencilBackRef == backRef) { return *this; } mStateCache.stencilFrontRef = frontRef; mStateCache.stencilBackRef = backRef; mCommands.push(CmdType::SetStencilRefVals).push(frontRef).push(backRef); return *this; } RenderCommandEncoder &RenderCommandEncoder::setStencilRefVal(uint32_t ref) { return setStencilRefVals(ref, ref); } RenderCommandEncoder &RenderCommandEncoder::setViewport(const MTLViewport &viewport) { if (mStateCache.viewport.valid() && mStateCache.viewport.value() == viewport) { return *this; } mStateCache.viewport = viewport; mCommands.push(CmdType::SetViewport).push(viewport); return *this; } RenderCommandEncoder &RenderCommandEncoder::setScissorRect(const MTLScissorRect &rect) { NSUInteger clampedWidth = rect.x > mRenderPassMaxScissorRect.width ? 0 : mRenderPassMaxScissorRect.width - rect.x; NSUInteger clampedHeight = rect.y > mRenderPassMaxScissorRect.height ? 0 : mRenderPassMaxScissorRect.height - rect.y; MTLScissorRect clampedRect = {rect.x, rect.y, std::min(rect.width, clampedWidth), std::min(rect.height, clampedHeight)}; if (mStateCache.scissorRect.valid() && mStateCache.scissorRect.value() == clampedRect) { return *this; } mStateCache.scissorRect = clampedRect; mCommands.push(CmdType::SetScissorRect).push(clampedRect); return *this; } RenderCommandEncoder &RenderCommandEncoder::setBlendColor(float r, float g, float b, float a) { if (mStateCache.blendColor[0] == r && mStateCache.blendColor[1] == g && mStateCache.blendColor[2] == b && mStateCache.blendColor[3] == a) { return *this; } mStateCache.blendColor[0] = r; mStateCache.blendColor[1] = g; mStateCache.blendColor[2] = b; mStateCache.blendColor[3] = a; mCommands.push(CmdType::SetBlendColor).push(r).push(g).push(b).push(a); return *this; } RenderCommandEncoder &RenderCommandEncoder::setBuffer(gl::ShaderType shaderType, const BufferRef &buffer, uint32_t offset, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } cmdBuffer().setReadDependency(buffer, /*isRenderCommand=*/true); id mtlBuffer = (buffer ? buffer->get() : nil); return commonSetBuffer(shaderType, mtlBuffer, offset, index); } RenderCommandEncoder &RenderCommandEncoder::setBufferForWrite(gl::ShaderType shaderType, const BufferRef &buffer, uint32_t offset, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } cmdBuffer().setWriteDependency(buffer, /*isRenderCommand=*/true); id mtlBuffer = (buffer ? buffer->get() : nil); return commonSetBuffer(shaderType, mtlBuffer, offset, index); } RenderCommandEncoder &RenderCommandEncoder::commonSetBuffer(gl::ShaderType shaderType, id mtlBuffer, uint32_t offset, uint32_t index) { RenderCommandEncoderShaderStates &shaderStates = mStateCache.perShaderStates[shaderType]; if (shaderStates.buffers[index] == mtlBuffer) { if (shaderStates.bufferOffsets[index] == offset) { return *this; } // If buffer already bound but with different offset, then update the offer only. shaderStates.bufferOffsets[index] = offset; mCommands.push(static_cast(mSetBufferOffsetCmds[shaderType])) .push(offset) .push(index); return *this; } shaderStates.buffers[index] = mtlBuffer; shaderStates.bufferOffsets[index] = offset; mCommands.push(static_cast(mSetBufferCmds[shaderType])) .push([mtlBuffer ANGLE_MTL_RETAIN]) .push(offset) .push(index); return *this; } RenderCommandEncoder &RenderCommandEncoder::setBytes(gl::ShaderType shaderType, const void *bytes, size_t size, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } RenderCommandEncoderShaderStates &shaderStates = mStateCache.perShaderStates[shaderType]; shaderStates.buffers[index] = nil; shaderStates.bufferOffsets[index] = 0; mCommands.push(static_cast(mSetBytesCmds[shaderType])) .push(size) .push(reinterpret_cast(bytes), size) .push(index); return *this; } RenderCommandEncoder &RenderCommandEncoder::setSamplerState(gl::ShaderType shaderType, id state, float lodMinClamp, float lodMaxClamp, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } RenderCommandEncoderShaderStates &shaderStates = mStateCache.perShaderStates[shaderType]; if (shaderStates.samplers[index] == state && shaderStates.samplerLodClamps[index].valid()) { const std::pair ¤tLodClampRange = shaderStates.samplerLodClamps[index].value(); if (currentLodClampRange.first == lodMinClamp && currentLodClampRange.second == lodMaxClamp) { return *this; } } shaderStates.samplers[index] = state; shaderStates.samplerLodClamps[index] = {lodMinClamp, lodMaxClamp}; mCommands.push(static_cast(mSetSamplerCmds[shaderType])) .push([state ANGLE_MTL_RETAIN]) .push(lodMinClamp) .push(lodMaxClamp) .push(index); return *this; } RenderCommandEncoder &RenderCommandEncoder::setTexture(gl::ShaderType shaderType, const TextureRef &texture, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } cmdBuffer().setReadDependency(texture, /*isRenderCommand=*/true); id mtlTexture = (texture ? texture->get() : nil); RenderCommandEncoderShaderStates &shaderStates = mStateCache.perShaderStates[shaderType]; if (shaderStates.textures[index] == mtlTexture) { return *this; } shaderStates.textures[index] = mtlTexture; mCommands.push(static_cast(mSetTextureCmds[shaderType])) .push([mtlTexture ANGLE_MTL_RETAIN]) .push(index); return *this; } RenderCommandEncoder &RenderCommandEncoder::setRWTexture(gl::ShaderType shaderType, const TextureRef &texture, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } cmdBuffer().setWriteDependency(texture, /*isRenderCommand=*/true); return setTexture(shaderType, texture, index); } RenderCommandEncoder &RenderCommandEncoder::draw(MTLPrimitiveType primitiveType, uint32_t vertexStart, uint32_t vertexCount) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); mHasDrawCalls = true; mCommands.push(CmdType::Draw).push(primitiveType).push(vertexStart).push(vertexCount); return *this; } RenderCommandEncoder &RenderCommandEncoder::drawInstanced(MTLPrimitiveType primitiveType, uint32_t vertexStart, uint32_t vertexCount, uint32_t instances) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); mHasDrawCalls = true; mCommands.push(CmdType::DrawInstanced) .push(primitiveType) .push(vertexStart) .push(vertexCount) .push(instances); return *this; } RenderCommandEncoder &RenderCommandEncoder::drawInstancedBaseInstance( MTLPrimitiveType primitiveType, uint32_t vertexStart, uint32_t vertexCount, uint32_t instances, uint32_t baseInstance) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); mHasDrawCalls = true; mCommands.push(CmdType::DrawInstancedBaseInstance) .push(primitiveType) .push(vertexStart) .push(vertexCount) .push(instances) .push(baseInstance); return *this; } RenderCommandEncoder &RenderCommandEncoder::drawIndexed(MTLPrimitiveType primitiveType, uint32_t indexCount, MTLIndexType indexType, const BufferRef &indexBuffer, size_t bufferOffset) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); if (!indexBuffer) { return *this; } mHasDrawCalls = true; cmdBuffer().setReadDependency(indexBuffer, /*isRenderCommand=*/true); mCommands.push(CmdType::DrawIndexed) .push(primitiveType) .push(indexCount) .push(indexType) .push([indexBuffer->get() ANGLE_MTL_RETAIN]) .push(bufferOffset); return *this; } RenderCommandEncoder &RenderCommandEncoder::drawIndexedInstanced(MTLPrimitiveType primitiveType, uint32_t indexCount, MTLIndexType indexType, const BufferRef &indexBuffer, size_t bufferOffset, uint32_t instances) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); if (!indexBuffer) { return *this; } mHasDrawCalls = true; cmdBuffer().setReadDependency(indexBuffer, /*isRenderCommand=*/true); mCommands.push(CmdType::DrawIndexedInstanced) .push(primitiveType) .push(indexCount) .push(indexType) .push([indexBuffer->get() ANGLE_MTL_RETAIN]) .push(bufferOffset) .push(instances); return *this; } RenderCommandEncoder &RenderCommandEncoder::drawIndexedInstancedBaseVertexBaseInstance( MTLPrimitiveType primitiveType, uint32_t indexCount, MTLIndexType indexType, const BufferRef &indexBuffer, size_t bufferOffset, uint32_t instances, uint32_t baseVertex, uint32_t baseInstance) { ASSERT(mPipelineStateSet && "Render Pipeline State was never set and we've issued a draw command."); CheckPrimitiveType(primitiveType); if (!indexBuffer) { return *this; } mHasDrawCalls = true; cmdBuffer().setReadDependency(indexBuffer, /*isRenderCommand=*/true); mCommands.push(CmdType::DrawIndexedInstancedBaseVertexBaseInstance) .push(primitiveType) .push(indexCount) .push(indexType) .push([indexBuffer->get() ANGLE_MTL_RETAIN]) .push(bufferOffset) .push(instances) .push(baseVertex) .push(baseInstance); return *this; } RenderCommandEncoder &RenderCommandEncoder::setVisibilityResultMode(MTLVisibilityResultMode mode, size_t offset) { if (mStateCache.visibilityResultMode == mode && mStateCache.visibilityResultBufferOffset == offset) { return *this; } mStateCache.visibilityResultMode = mode; mStateCache.visibilityResultBufferOffset = offset; mCommands.push(CmdType::SetVisibilityResultMode).push(mode).push(offset); return *this; } RenderCommandEncoder &RenderCommandEncoder::useResource(const BufferRef &resource, MTLResourceUsage usage, MTLRenderStages stages) { if (!resource) { return *this; } cmdBuffer().setReadDependency(resource, /*isRenderCommand=*/true); mCommands.push(CmdType::UseResource) .push([resource->get() ANGLE_MTL_RETAIN]) .push(usage) .push(stages); return *this; } RenderCommandEncoder &RenderCommandEncoder::memoryBarrier(MTLBarrierScope scope, MTLRenderStages after, MTLRenderStages before) { mCommands.push(CmdType::MemoryBarrier).push(scope).push(after).push(before); return *this; } RenderCommandEncoder &RenderCommandEncoder::memoryBarrierWithResource(const BufferRef &resource, MTLRenderStages after, MTLRenderStages before) { if (!resource) { return *this; } cmdBuffer().setWriteDependency(resource, /*isRenderCommand=*/true); mCommands.push(CmdType::MemoryBarrierWithResource) .push([resource->get() ANGLE_MTL_RETAIN]) .push(after) .push(before); return *this; } void RenderCommandEncoder::insertDebugSignpostImpl(NSString *label) { // Defer the insertion until endEncoding() mCommands.push(CmdType::InsertDebugSignpost).push([label ANGLE_MTL_RETAIN]); } void RenderCommandEncoder::pushDebugGroup(NSString *label) { // Defer the insertion until endEncoding() mCommands.push(CmdType::PushDebugGroup).push([label ANGLE_MTL_RETAIN]); } void RenderCommandEncoder::popDebugGroup() { mCommands.push(CmdType::PopDebugGroup); } RenderCommandEncoder &RenderCommandEncoder::setColorStoreAction(MTLStoreAction action, uint32_t colorAttachmentIndex) { if (colorAttachmentIndex >= mRenderPassDesc.numColorAttachments) { return *this; } // We only store the options, will defer the actual setting until the encoder finishes mRenderPassDesc.colorAttachments[colorAttachmentIndex].storeAction = action; return *this; } RenderCommandEncoder &RenderCommandEncoder::setColorStoreAction(MTLStoreAction action) { for (uint32_t i = 0; i < mRenderPassDesc.numColorAttachments; ++i) { setColorStoreAction(action, i); } return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthStencilStoreAction( MTLStoreAction depthStoreAction, MTLStoreAction stencilStoreAction) { // We only store the options, will defer the actual setting until the encoder finishes mRenderPassDesc.depthAttachment.storeAction = depthStoreAction; mRenderPassDesc.stencilAttachment.storeAction = stencilStoreAction; return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthStoreAction(MTLStoreAction action) { // We only store the options, will defer the actual setting until the encoder finishes mRenderPassDesc.depthAttachment.storeAction = action; return *this; } RenderCommandEncoder &RenderCommandEncoder::setStencilStoreAction(MTLStoreAction action) { // We only store the options, will defer the actual setting until the encoder finishes mRenderPassDesc.stencilAttachment.storeAction = action; return *this; } RenderCommandEncoder &RenderCommandEncoder::setStoreAction(MTLStoreAction action) { setColorStoreAction(action); setDepthStencilStoreAction(action, action); return *this; } RenderCommandEncoder &RenderCommandEncoder::setColorLoadAction(MTLLoadAction action, const MTLClearColor &clearValue, uint32_t colorAttachmentIndex) { ASSERT(!hasDrawCalls()); if (mCachedRenderPassDescObjC.get().colorAttachments[colorAttachmentIndex].texture) { mCachedRenderPassDescObjC.get().colorAttachments[colorAttachmentIndex].loadAction = action; mCachedRenderPassDescObjC.get().colorAttachments[colorAttachmentIndex].clearColor = clearValue; } return *this; } RenderCommandEncoder &RenderCommandEncoder::setDepthLoadAction(MTLLoadAction action, double clearVal) { ASSERT(!hasDrawCalls()); if (mCachedRenderPassDescObjC.get().depthAttachment.texture) { mCachedRenderPassDescObjC.get().depthAttachment.loadAction = action; mCachedRenderPassDescObjC.get().depthAttachment.clearDepth = clearVal; } return *this; } RenderCommandEncoder &RenderCommandEncoder::setStencilLoadAction(MTLLoadAction action, uint32_t clearVal) { ASSERT(!hasDrawCalls()); if (mCachedRenderPassDescObjC.get().stencilAttachment.texture) { mCachedRenderPassDescObjC.get().stencilAttachment.loadAction = action; mCachedRenderPassDescObjC.get().stencilAttachment.clearStencil = clearVal; } return *this; } void RenderCommandEncoder::setLabel(NSString *label) { mLabel = std::move(label); } // BlitCommandEncoder BlitCommandEncoder::BlitCommandEncoder(CommandBuffer *cmdBuffer) : CommandEncoder(cmdBuffer, BLIT) {} BlitCommandEncoder::~BlitCommandEncoder() {} BlitCommandEncoder &BlitCommandEncoder::restart() { ANGLE_MTL_OBJC_SCOPE { if (valid()) { // no change, skip return *this; } if (!cmdBuffer().ready()) { reset(); return *this; } // Create objective C object set([cmdBuffer().get() blitCommandEncoder]); // Verify that it was created successfully ASSERT(get()); return *this; } } BlitCommandEncoder &BlitCommandEncoder::copyBuffer(const BufferRef &src, size_t srcOffset, const BufferRef &dst, size_t dstOffset, size_t size) { if (!src || !dst) { return *this; } cmdBuffer().setReadDependency(src, /*isRenderCommand=*/false); cmdBuffer().setWriteDependency(dst, /*isRenderCommand=*/false); [get() copyFromBuffer:src->get() sourceOffset:srcOffset toBuffer:dst->get() destinationOffset:dstOffset size:size]; return *this; } BlitCommandEncoder &BlitCommandEncoder::copyBufferToTexture(const BufferRef &src, size_t srcOffset, size_t srcBytesPerRow, size_t srcBytesPerImage, MTLSize srcSize, const TextureRef &dst, uint32_t dstSlice, MipmapNativeLevel dstLevel, MTLOrigin dstOrigin, MTLBlitOption blitOption) { if (!src || !dst) { return *this; } cmdBuffer().setReadDependency(src, /*isRenderCommand=*/false); cmdBuffer().setWriteDependency(dst, /*isRenderCommand=*/false); [get() copyFromBuffer:src->get() sourceOffset:srcOffset sourceBytesPerRow:srcBytesPerRow sourceBytesPerImage:srcBytesPerImage sourceSize:srcSize toTexture:dst->get() destinationSlice:dstSlice destinationLevel:dstLevel.get() destinationOrigin:dstOrigin options:blitOption]; return *this; } BlitCommandEncoder &BlitCommandEncoder::copyTextureToBuffer(const TextureRef &src, uint32_t srcSlice, MipmapNativeLevel srcLevel, MTLOrigin srcOrigin, MTLSize srcSize, const BufferRef &dst, size_t dstOffset, size_t dstBytesPerRow, size_t dstBytesPerImage, MTLBlitOption blitOption) { if (!src || !dst) { return *this; } cmdBuffer().setReadDependency(src, /*isRenderCommand=*/false); cmdBuffer().setWriteDependency(dst, /*isRenderCommand=*/false); [get() copyFromTexture:src->get() sourceSlice:srcSlice sourceLevel:srcLevel.get() sourceOrigin:srcOrigin sourceSize:srcSize toBuffer:dst->get() destinationOffset:dstOffset destinationBytesPerRow:dstBytesPerRow destinationBytesPerImage:dstBytesPerImage options:blitOption]; return *this; } BlitCommandEncoder &BlitCommandEncoder::copyTexture(const TextureRef &src, uint32_t srcStartSlice, MipmapNativeLevel srcStartLevel, const TextureRef &dst, uint32_t dstStartSlice, MipmapNativeLevel dstStartLevel, uint32_t sliceCount, uint32_t levelCount) { if (!src || !dst) { return *this; } cmdBuffer().setReadDependency(src, /*isRenderCommand=*/false); cmdBuffer().setWriteDependency(dst, /*isRenderCommand=*/false); MTLOrigin origin = MTLOriginMake(0, 0, 0); for (uint32_t slice = 0; slice < sliceCount; ++slice) { uint32_t srcSlice = srcStartSlice + slice; uint32_t dstSlice = dstStartSlice + slice; for (uint32_t level = 0; level < levelCount; ++level) { MipmapNativeLevel srcLevel = srcStartLevel + level; MipmapNativeLevel dstLevel = dstStartLevel + level; MTLSize srcSize = MTLSizeMake(src->width(srcLevel), src->height(srcLevel), src->depth(srcLevel)); [get() copyFromTexture:src->get() sourceSlice:srcSlice sourceLevel:srcLevel.get() sourceOrigin:origin sourceSize:srcSize toTexture:dst->get() destinationSlice:dstSlice destinationLevel:dstLevel.get() destinationOrigin:origin]; } } return *this; } BlitCommandEncoder &BlitCommandEncoder::fillBuffer(const BufferRef &buffer, NSRange range, uint8_t value) { if (!buffer) { return *this; } cmdBuffer().setWriteDependency(buffer, /*isRenderCommand=*/false); [get() fillBuffer:buffer->get() range:range value:value]; return *this; } BlitCommandEncoder &BlitCommandEncoder::generateMipmapsForTexture(const TextureRef &texture) { if (!texture) { return *this; } cmdBuffer().setWriteDependency(texture, /*isRenderCommand=*/false); [get() generateMipmapsForTexture:texture->get()]; return *this; } BlitCommandEncoder &BlitCommandEncoder::synchronizeResource(Buffer *buffer) { if (!buffer) { return *this; } #if TARGET_OS_OSX || TARGET_OS_MACCATALYST if (buffer->get().storageMode == MTLStorageModeManaged) { // Only MacOS has separated storage for resource on CPU and GPU and needs explicit // synchronization cmdBuffer().setReadDependency(buffer, /*isRenderCommand=*/false); [get() synchronizeResource:buffer->get()]; } #endif return *this; } BlitCommandEncoder &BlitCommandEncoder::synchronizeResource(Texture *texture) { if (!texture) { return *this; } #if TARGET_OS_OSX || TARGET_OS_MACCATALYST // Only MacOS has separated storage for resource on CPU and GPU and needs explicit // synchronization cmdBuffer().setReadDependency(texture, /*isRenderCommand=*/false); if (texture->get().parentTexture) { [get() synchronizeResource:texture->get().parentTexture]; } else { [get() synchronizeResource:texture->get()]; } #endif return *this; } // ComputeCommandEncoder implementation ComputeCommandEncoder::ComputeCommandEncoder(CommandBuffer *cmdBuffer) : CommandEncoder(cmdBuffer, COMPUTE) {} ComputeCommandEncoder::~ComputeCommandEncoder() {} ComputeCommandEncoder &ComputeCommandEncoder::restart() { ANGLE_MTL_OBJC_SCOPE { if (valid()) { // no change, skip return *this; } if (!cmdBuffer().ready()) { reset(); return *this; } // Create objective C object set([cmdBuffer().get() computeCommandEncoder]); // Verify that it was created successfully ASSERT(get()); return *this; } } ComputeCommandEncoder &ComputeCommandEncoder::setComputePipelineState( id state) { [get() setComputePipelineState:state]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::setBuffer(const BufferRef &buffer, uint32_t offset, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } cmdBuffer().setReadDependency(buffer, /*isRenderCommand=*/false); [get() setBuffer:(buffer ? buffer->get() : nil) offset:offset atIndex:index]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::setBufferForWrite(const BufferRef &buffer, uint32_t offset, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } cmdBuffer().setWriteDependency(buffer, /*isRenderCommand=*/false); return setBuffer(buffer, offset, index); } ComputeCommandEncoder &ComputeCommandEncoder::setBytes(const void *bytes, size_t size, uint32_t index) { if (index >= kMaxShaderBuffers) { return *this; } [get() setBytes:bytes length:size atIndex:index]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::setSamplerState(id state, float lodMinClamp, float lodMaxClamp, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } [get() setSamplerState:state lodMinClamp:lodMinClamp lodMaxClamp:lodMaxClamp atIndex:index]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::setTexture(const TextureRef &texture, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } cmdBuffer().setReadDependency(texture, /*isRenderCommand=*/false); [get() setTexture:(texture ? texture->get() : nil) atIndex:index]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::setTextureForWrite(const TextureRef &texture, uint32_t index) { if (index >= kMaxShaderSamplers) { return *this; } cmdBuffer().setWriteDependency(texture, /*isRenderCommand=*/false); return setTexture(texture, index); } ComputeCommandEncoder &ComputeCommandEncoder::dispatch(const MTLSize &threadGroupsPerGrid, const MTLSize &threadsPerGroup) { [get() dispatchThreadgroups:threadGroupsPerGrid threadsPerThreadgroup:threadsPerGroup]; return *this; } ComputeCommandEncoder &ComputeCommandEncoder::dispatchNonUniform(const MTLSize &threadsPerGrid, const MTLSize &threadsPerGroup) { [get() dispatchThreads:threadsPerGrid threadsPerThreadgroup:threadsPerGroup]; return *this; } } // namespace mtl } // namespace rx